# Copyright Spack Project Developers. See COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import contextlib
import filecmp
import glob
import io
import os
import pathlib
import shutil
import sys
from argparse import Namespace
from typing import Any, Dict, Optional

import pytest

import spack.cmd.env
import spack.concretize
import spack.config
import spack.environment as ev
import spack.environment.depfile as depfile
import spack.error
import spack.llnl.util.filesystem as fs
import spack.llnl.util.link_tree
import spack.llnl.util.tty as tty
import spack.main
import spack.modules
import spack.modules.tcl
import spack.package_base
import spack.paths
import spack.repo
import spack.schema.env
import spack.solver.asp
import spack.stage
import spack.store
import spack.util.environment
import spack.util.spack_json as sjson
import spack.util.spack_yaml
from spack.cmd.env import _env_create
from spack.installer import PackageInstaller
from spack.llnl.util.filesystem import readlink
from spack.llnl.util.lang import dedupe
from spack.main import SpackCommand, SpackCommandError
from spack.spec import Spec
from spack.stage import stage_prefix
from spack.test.conftest import RepoBuilder
from spack.traverse import traverse_nodes
from spack.util.executable import Executable
from spack.util.path import substitute_path_variables
from spack.version import Version

# TODO-27021
# everything here uses the mock_env_path
pytestmark = [
    pytest.mark.usefixtures("mutable_config", "mutable_mock_env_path", "mutable_mock_repo"),
    pytest.mark.maybeslow,
    pytest.mark.not_on_windows("Envs unsupported on Windows"),
]

env = SpackCommand("env")
install = SpackCommand("install")
add = SpackCommand("add")
change = SpackCommand("change")
config = SpackCommand("config")
remove = SpackCommand("remove")
concretize = SpackCommand("concretize")
stage = SpackCommand("stage")
uninstall = SpackCommand("uninstall")
find = SpackCommand("find")
develop = SpackCommand("develop")
module = SpackCommand("module")

sep = os.sep


def setup_combined_multiple_env():
    env("create", "test1")
    test1 = ev.read("test1")
    with test1:
        add("mpich@1.0")
        test1.concretize()
        test1.write()

    env("create", "test2")
    test2 = ev.read("test2")
    with test2:
        add("libelf")
        test2.concretize()
        test2.write()

    env("create", "--include-concrete", "test1", "--include-concrete", "test2", "combined_env")
    combined = ev.read("combined_env")
    return test1, test2, combined


@pytest.fixture()
def environment_from_manifest(tmp_path: pathlib.Path):
    """Returns a new environment named 'test' from the content of a manifest file."""

    def _create(content):
        spack_yaml = tmp_path / ev.manifest_name
        spack_yaml.write_text(content)
        return _env_create("test", init_file=str(spack_yaml))

    return _create


def check_mpileaks_and_deps_in_view(viewdir: pathlib.Path):
    """Check that the expected install directories exist."""
    assert (viewdir / ".spack" / "mpileaks").exists()
    assert (viewdir / ".spack" / "libdwarf").exists()


def check_viewdir_removal(viewdir: pathlib.Path):
    """Check that the uninstall/removal worked."""
    assert not (viewdir / ".spack").exists() or list((viewdir / ".spack").iterdir()) == [
        viewdir / "projections.yaml"
    ]


def test_env_track_nonexistent_path_fails():
    with pytest.raises(spack.main.SpackCommandError):
        env("track", "path/does/not/exist")

    assert "doesn't contain an environment" in env.output


def test_env_track_existing_env_fails():
    env("create", "track_test")

    with pytest.raises(spack.main.SpackCommandError):
        env("track", "--name", "track_test", ev.environment_dir_from_name("track_test"))

    assert "environment named track_test already exists" in env.output


def test_env_track_valid(tmp_path: pathlib.Path):
    with fs.working_dir(str(tmp_path)):
        # create an independent environment
        env("create", "-d", ".")

        # test tracking an environment in known store
        env("track", "--name", "test1", ".")

        # test removing environment to ensure independent isn't deleted
        env("rm", "-y", "test1")

        assert os.path.isfile("spack.yaml")


def test_env_untrack_valid(tmp_path: pathlib.Path):
    with fs.working_dir(str(tmp_path)):
        # create an independent environment
        env("create", "-d", ".")

        # test tracking an environment in known store
        env("track", "--name", "test_untrack", ".")
        env("untrack", "--yes-to-all", "test_untrack")

        # check that environment was successfully untracked
        out = env("ls")
        assert "test_untrack" not in out


def test_env_untrack_invalid_name():
    # test untracking an environment that doesn't exist
    env_name = "invalid_environment_untrack"

    out = env("untrack", env_name)

    assert f"Environment '{env_name}' does not exist" in out


def test_env_untrack_when_active(tmp_path: pathlib.Path):
    env_name = "test_untrack_active"

    with fs.working_dir(str(tmp_path)):
        # create an independent environment
        env("create", "-d", ".")

        # test tracking an environment in known store
        env("track", "--name", env_name, ".")

        active_env = ev.read(env_name)
        with active_env:
            output = env("untrack", "--yes-to-all", env_name, fail_on_error=False)
            assert env.error is not None

        # check that environment could not be untracked while active
        assert f"'{env_name}' can't be untracked while activated" in output

        env("untrack", "-f", env_name)
        out = env("ls")
        assert env_name not in out


def test_env_untrack_managed():
    env_name = "test_untrack_managed"

    # create an managed environment
    env("create", env_name)

    with pytest.raises(spack.main.SpackCommandError):
        env("untrack", env_name)

    # check that environment could not be untracked while active
    assert f"'{env_name}' is not a tracked env" in env.output


@pytest.fixture()
def installed_environment(
    tmp_path: pathlib.Path, mock_fetch, mock_packages, mock_archive, install_mockery
):
    spack_yaml = tmp_path / "spack.yaml"

    @contextlib.contextmanager
    def _installed_environment(content):
        spack_yaml.write_text(content)
        with fs.working_dir(tmp_path):
            env("create", "test", "./spack.yaml")
            with ev.read("test") as current_environment:
                current_environment.concretize()
                current_environment.install_all(fake=True)
                current_environment.write(regenerate=True)

            with ev.read("test") as current_environment:
                yield current_environment

    return _installed_environment


@pytest.fixture()
def template_combinatorial_env(tmp_path: pathlib.Path):
    """Returns a template base environment for tests. Since the environment configuration is
    extended using str.format, we need double '{' escaping for the projections.
    """
    view_dir = tmp_path / "view"
    return f"""\
    spack:
      definitions:
        - packages: [mpileaks, callpath]
        - targets: ['target=x86_64', 'target=core2']
      specs:
        - matrix:
            - [$packages]
            - [$targets]

      view:
        combinatorial:
          root: {view_dir}
          {{view_config}}
          projections:
            'all': '{{{{architecture.target}}}}/{{{{name}}}}-{{{{version}}}}'
    """


def test_add_requires_active_env():
    """Test that spack add exits with code 2 when no environment is active."""
    add("hdf5", fail_on_error=False)
    assert add.returncode == 2


def test_add():
    e = ev.create("test")
    e.add("mpileaks")
    assert Spec("mpileaks") in e.user_specs


def test_change_match_spec():
    env("create", "test")

    e = ev.read("test")
    with e:
        add("mpileaks@2.1")
        add("mpileaks@2.2")

        change("--match-spec", "mpileaks@2.2", "mpileaks@2.3")

    assert not any(x.intersects("mpileaks@2.2") for x in e.user_specs)
    assert any(x.intersects("mpileaks@2.3") for x in e.user_specs)


def test_change_multiple_matches():
    env("create", "test")

    e = ev.read("test")
    with e:
        add("mpileaks@2.1")
        add("mpileaks@2.2")
        add("libelf@0.8.12%clang")

        change("--match-spec", "mpileaks", "-a", "mpileaks%gcc")

    assert all(x.intersects("%gcc") for x in e.user_specs if x.name == "mpileaks")
    assert any(x.intersects("%clang") for x in e.user_specs if x.name == "libelf")


def test_env_add_virtual():
    env("create", "test")
    e = ev.read("test")
    e.add("mpi")
    e.concretize()

    assert len(e.concretized_roots) == 1
    spec = e.specs_by_hash[e.concretized_roots[0].hash]
    assert spec.intersects("mpi")


def test_env_add_nonexistent_fails():
    env("create", "test")

    e = ev.read("test")
    with pytest.raises(ev.SpackEnvironmentError, match=r"no such package"):
        e.add("thispackagedoesnotexist")


def test_env_list(mutable_mock_env_path):
    env("create", "foo")
    env("create", "bar")
    env("create", "baz")

    out = env("list")

    assert "foo" in out
    assert "bar" in out
    assert "baz" in out

    # make sure `spack env list` skips invalid things in var/spack/env
    (mutable_mock_env_path / ".DS_Store").touch()
    out = env("list")

    assert "foo" in out
    assert "bar" in out
    assert "baz" in out
    assert ".DS_Store" not in out


def test_env_remove():
    env("create", "foo")
    env("create", "bar")

    out = env("list")
    assert "foo" in out
    assert "bar" in out

    foo = ev.read("foo")
    with foo:
        with pytest.raises(SpackCommandError):
            env("remove", "-y", "foo")
        assert "foo" in env("list")

    env("remove", "-y", "foo")
    out = env("list")
    assert "foo" not in out
    assert "bar" in out

    env("remove", "-y", "bar")
    out = env("list")
    assert "foo" not in out
    assert "bar" not in out


def test_env_rename_managed():
    # Need real environment
    with pytest.raises(spack.main.SpackCommandError):
        env("rename", "foo", "bar")
    assert "The specified name does not correspond to a managed spack environment" in env.output

    env("create", "foo")

    out = env("list")
    assert "foo" in out

    out = env("rename", "foo", "bar")
    assert "Successfully renamed environment foo to bar" in out

    out = env("list")
    assert "foo" not in out
    assert "bar" in out

    bar = ev.read("bar")
    with bar:
        # Cannot rename active environment
        with pytest.raises(spack.main.SpackCommandError):
            env("rename", "bar", "baz")
        assert "Cannot rename active environment" in env.output

        env("create", "qux")

        # Cannot rename to an active environment (even with force flag)
        with pytest.raises(spack.main.SpackCommandError):
            env("rename", "-f", "qux", "bar")
        assert "bar is an active environment" in env.output

        # Can rename inactive environment when another's active
        out = env("rename", "qux", "quux")
        assert "Successfully renamed environment qux to quux" in out

    out = env("list")
    assert "bar" in out
    assert "baz" not in out

    env("create", "baz")

    # Cannot rename to existing environment without --force
    with pytest.raises(spack.main.SpackCommandError):
        env("rename", "bar", "baz")
    errmsg = (
        "The new name corresponds to an existing environment;"
        " specify the --force flag to overwrite it."
    )
    assert errmsg in env.output

    env("rename", "-f", "bar", "baz")
    out = env("list")
    assert "bar" not in out
    assert "baz" in out


def test_env_rename_independent(tmp_path: pathlib.Path):
    # Need real environment
    with pytest.raises(spack.main.SpackCommandError):
        env("rename", "-d", "./non-existing", "./also-non-existing")
    assert "The specified path does not correspond to a valid spack environment" in env.output

    anon_foo = str(tmp_path / "foo")
    env("create", "-d", anon_foo)

    anon_bar = str(tmp_path / "bar")
    out = env("rename", "-d", anon_foo, anon_bar)
    assert f"Successfully renamed environment {anon_foo} to {anon_bar}" in out
    assert not ev.is_env_dir(anon_foo)
    assert ev.is_env_dir(anon_bar)

    # Cannot rename active environment
    anon_baz = str(tmp_path / "baz")
    env("activate", "--sh", "-d", anon_bar)
    with pytest.raises(spack.main.SpackCommandError):
        env("rename", "-d", anon_bar, anon_baz)
    assert "Cannot rename active environment" in env.output
    env("deactivate", "--sh")

    assert ev.is_env_dir(anon_bar)
    assert not ev.is_env_dir(anon_baz)

    # Cannot rename to existing environment without --force
    env("create", "-d", anon_baz)
    with pytest.raises(spack.main.SpackCommandError):
        env("rename", "-d", anon_bar, anon_baz)
    errmsg = (
        "The new path corresponds to an existing environment;"
        " specify the --force flag to overwrite it."
    )
    assert errmsg in env.output
    assert ev.is_env_dir(anon_bar)
    assert ev.is_env_dir(anon_baz)

    env("rename", "-f", "-d", anon_bar, anon_baz)
    assert not ev.is_env_dir(anon_bar)
    assert ev.is_env_dir(anon_baz)

    # Cannot rename to existing (non-environment) path without --force
    qux = tmp_path / "qux"
    qux.mkdir()
    anon_qux = str(qux)
    assert not ev.is_env_dir(anon_qux)

    with pytest.raises(spack.main.SpackCommandError):
        env("rename", "-d", anon_baz, anon_qux)
    errmsg = "The new path already exists; specify the --force flag to overwrite it."
    assert errmsg in env.output

    env("rename", "-f", "-d", anon_baz, anon_qux)
    assert not ev.is_env_dir(anon_baz)
    assert ev.is_env_dir(anon_qux)


def test_concretize():
    e = ev.create("test")
    e.add("mpileaks")
    e.concretize()

    assert len(e.concretized_roots) == 1
    assert e.concretized_roots[0].root == Spec("mpileaks")


def test_env_specs_partition(install_mockery, mock_fetch):
    e = ev.create("test")
    e.add("cmake-client")
    e.concretize()

    # Single not installed root spec.
    roots_already_installed, roots_to_install = e._partition_roots_by_install_status()
    assert len(roots_already_installed) == 0
    assert len(roots_to_install) == 1
    assert roots_to_install[0].name == "cmake-client"

    # Single installed root.
    e.install_all(fake=True)
    roots_already_installed, roots_to_install = e._partition_roots_by_install_status()
    assert len(roots_already_installed) == 1
    assert roots_already_installed[0].name == "cmake-client"
    assert len(roots_to_install) == 0

    # One installed root, one not installed root.
    e.add("mpileaks")
    e.concretize()
    roots_already_installed, roots_to_install = e._partition_roots_by_install_status()
    assert len(roots_already_installed) == 1
    assert len(roots_to_install) == 1
    assert roots_already_installed[0].name == "cmake-client"
    assert roots_to_install[0].name == "mpileaks"


def test_env_install_all(install_mockery, mock_fetch):
    e = ev.create("test")
    e.add("cmake-client")
    e.concretize()
    e.install_all(fake=True)
    spec = next(x for x in e.all_specs_generator() if x.name == "cmake-client")
    assert spec.installed


def test_env_install_single_spec(install_mockery, mock_fetch, installer_variant):
    env("create", "test")
    install = SpackCommand("install")

    e = ev.read("test")
    with e:
        install("--fake", "--add", "cmake-client")

    e = ev.read("test")
    assert len(e.concretized_roots) == 1

    item = e.concretized_roots[0]
    assert list(e.user_specs) == [Spec("cmake-client")]
    assert item.root == Spec("cmake-client")
    assert e.specs_by_hash[item.hash].name == "cmake-client"


@pytest.mark.parametrize("unify", [True, False, "when_possible"])
@pytest.mark.parametrize("reuse", [True, False])
def test_env_install_include_concrete_env(
    unify, reuse, install_mockery, mock_fetch, mutable_config
):
    test1, test2, combined = setup_combined_multiple_env()

    if unify is False:
        combined.manifest.set_default_view(False)

    with combined:
        mutable_config.set("concretizer:unify", unify)
        mutable_config.set("concretizer:reuse", reuse)
        combined.add("mpileaks")
        combined.concretize()
        combined.write()
        install("--fake")

    test1_user_spec_hashes = [x.hash for x in test1.concretized_roots]
    test2_user_spec_hashes = [x.hash for x in test2.concretized_roots]

    for spec in combined.all_specs():
        assert spec.installed

    assert test1_user_spec_hashes == [
        x.hash for x in combined.included_concretized_roots[test1.path]
    ]
    assert test2_user_spec_hashes == [
        x.hash for x in combined.included_concretized_roots[test2.path]
    ]

    mpileaks_hash = combined.concretized_roots[0].hash
    mpileaks = combined.specs_by_hash[mpileaks_hash]
    if unify is False and reuse is False:
        # check that unification is not by accident
        assert mpileaks["mpi"].dag_hash() not in test1_user_spec_hashes
    else:
        assert mpileaks["mpi"].dag_hash() in test1_user_spec_hashes
        assert mpileaks["libelf"].dag_hash() in test2_user_spec_hashes


def test_env_roots_marked_explicit(install_mockery, mock_fetch, installer_variant):
    install = SpackCommand("install")
    install("--fake", "dependent-install")

    # Check one explicit, one implicit install
    dependent = spack.store.STORE.db.query(explicit=True)
    dependency = spack.store.STORE.db.query(explicit=False)
    assert len(dependent) == 1
    assert len(dependency) == 1

    env("create", "test")
    with ev.read("test") as e:
        # make implicit install a root of the env
        e.add(dependency[0].name)
        e.concretize()
        e.install_all()

    explicit = spack.store.STORE.db.query(explicit=True)
    assert len(explicit) == 2


def test_env_modifications_error_on_activate(install_mockery, mock_fetch, monkeypatch, capfd):
    env("create", "test")
    install = SpackCommand("install")

    e = ev.read("test")
    with e:
        install("--fake", "--add", "cmake-client")

    def setup_error(pkg, env):
        raise RuntimeError("cmake-client had issues!")

    pkg = spack.repo.PATH.get_pkg_class("cmake-client")
    monkeypatch.setattr(pkg, "setup_run_environment", setup_error)

    ev.shell.activate(e)

    _, err = capfd.readouterr()
    assert "cmake-client had issues!" in err
    assert "Warning: could not load runtime environment" in err


def test_activate_adds_transitive_run_deps_to_path(install_mockery, mock_fetch, monkeypatch):
    env("create", "test")
    install = SpackCommand("install")

    e = ev.read("test")
    with e:
        install("--add", "--fake", "depends-on-run-env")

    env_variables = {}
    ev.shell.activate(e).apply_modifications(env_variables)
    assert env_variables["DEPENDENCY_ENV_VAR"] == "1"


def test_env_definition_symlink(install_mockery, mock_fetch, tmp_path: pathlib.Path):
    filepath = str(tmp_path / "spack.yaml")
    filepath_mid = str(tmp_path / "spack_mid.yaml")

    env("create", "test")
    e = ev.read("test")
    e.add("mpileaks")

    os.rename(e.manifest_path, filepath)
    os.symlink(filepath, filepath_mid)
    os.symlink(filepath_mid, e.manifest_path)

    e.concretize()
    e.write()

    assert os.path.islink(e.manifest_path)
    assert os.path.islink(filepath_mid)


def test_env_install_two_specs_same_dep(
    install_mockery, mock_fetch, tmp_path: pathlib.Path, monkeypatch
):
    """Test installation of two packages that share a dependency with no
    connection and the second specifying the dependency as a 'build'
    dependency.
    """
    path = tmp_path / "spack.yaml"

    with fs.working_dir(str(tmp_path)):
        with open(str(path), "w", encoding="utf-8") as f:
            f.write(
                """\
spack:
  specs:
  - pkg-a
  - depb
"""
            )

        env("create", "test", "spack.yaml")

    with ev.read("test"):
        out = install("--fake")

    # Ensure both packages reach install phase processing and are installed
    out = str(out)
    assert "depb: Successfully installed" in out
    assert "pkg-a: Successfully installed" in out

    depb = spack.store.STORE.db.query_one("depb", installed=True)
    assert depb, "Expected depb to be installed"

    a = spack.store.STORE.db.query_one("pkg-a", installed=True)
    assert a, "Expected pkg-a to be installed"


def test_remove_after_concretize():
    e = ev.create("test")

    e.add("mpileaks")
    e.concretize()

    e.add("python")
    e.concretize()

    e.remove("mpileaks")
    assert Spec("mpileaks") not in e.user_specs
    assert any(s.name == "mpileaks" for s in e.all_specs_generator())

    e.add("mpileaks")
    assert any(s.name == "mpileaks" for s in e.user_specs)

    e.remove("mpileaks", force=True)
    assert Spec("mpileaks") not in e.user_specs
    assert not any(s.name == "mpileaks" for s in e.all_specs_generator())


def test_remove_before_concretize(mutable_config):
    """Tests the effect of concretization after adding and removing specs"""
    with ev.create("test") as e:
        mutable_config.set("concretizer:unify", True)
        e.add("mpileaks")
        e.concretize()
        assert len(e.concretized_roots) == 1
        assert e.concrete_roots()[0].satisfies("mpileaks")

        e.remove("mpileaks")
        e.concretize()
        assert not e.concretized_roots


def test_remove_command():
    env("create", "test")
    assert "test" in env("list")

    with ev.read("test"):
        add("mpileaks")

    with ev.read("test"):
        assert "mpileaks" in find()
        assert "mpileaks@" not in find()
        assert "mpileaks@" not in find("--show-concretized")

    with ev.read("test"):
        remove("mpileaks")

    with ev.read("test"):
        assert "mpileaks" not in find()
        assert "mpileaks@" not in find()
        assert "mpileaks@" not in find("--show-concretized")

    with ev.read("test"):
        add("mpileaks")

    with ev.read("test"):
        assert "mpileaks" in find()
        assert "mpileaks@" not in find()
        assert "mpileaks@" not in find("--show-concretized")

    with ev.read("test"):
        concretize()

    with ev.read("test"):
        assert "mpileaks" in find()
        assert "mpileaks@" not in find()
        assert "mpileaks@" in find("--show-concretized")

    with ev.read("test"):
        remove("mpileaks")

    with ev.read("test"):
        assert "mpileaks" not in find()
        # removed but still in last concretized specs
        assert "mpileaks@" in find("--show-concretized")

    with ev.read("test"):
        concretize()

    with ev.read("test"):
        assert "mpileaks" not in find()
        assert "mpileaks@" not in find()
        # now the lockfile is regenerated and it's gone.
        assert "mpileaks@" not in find("--show-concretized")


def test_remove_command_all():
    # Need separate ev.read calls for each command to ensure we test round-trip to disk
    env("create", "test")
    test_pkgs = ("mpileaks", "zlib")

    with ev.read("test"):
        for name in test_pkgs:
            add(name)

    with ev.read("test"):
        for name in test_pkgs:
            assert name in find()
            assert f"{name}@" not in find()

    with ev.read("test"):
        remove("-a")

    with ev.read("test"):
        for name in test_pkgs:
            assert name not in find()


def test_bad_remove_included_env():
    env("create", "test")
    test = ev.read("test")

    with test:
        add("mpileaks")

    test.concretize()
    test.write()

    env("create", "--include-concrete", "test", "combined_env")

    with pytest.raises(SpackCommandError):
        env("remove", "test")


def test_force_remove_included_env():
    env("create", "test")
    test = ev.read("test")

    with test:
        add("mpileaks")

    test.concretize()
    test.write()

    env("create", "--include-concrete", "test", "combined_env")

    rm_output = env("remove", "-f", "-y", "test")
    list_output = env("list")

    assert "'test' is used by environment 'combined_env'" in rm_output
    assert "test" not in list_output


def test_environment_status(tmp_path: pathlib.Path, monkeypatch):
    with fs.working_dir(str(tmp_path)):
        assert "No active environment" in env("status")

        with ev.create("test"):
            assert "In environment test" in env("status")

        with ev.create_in_dir("local_dir"):
            assert os.path.join(os.getcwd(), "local_dir") in env("status")

            e = ev.create_in_dir("myproject")
            e.write()
            with fs.working_dir(str(tmp_path / "myproject")):
                with e:
                    assert "in current directory" in env("status")


def test_env_status_broken_view(
    mutable_mock_env_path,
    mock_archive,
    mock_fetch,
    mock_custom_repository,
    install_mockery,
    tmp_path: pathlib.Path,
):
    with ev.create_in_dir(tmp_path):
        install("--add", "--fake", "trivial-install-test-package")

    # switch to a new repo that doesn't include the installed package
    # test that Spack detects the missing package and warns the user
    with spack.repo.use_repositories(mock_custom_repository):
        with ev.Environment(tmp_path):
            output = env("status")
            assert "includes out of date packages or repos" in output

    # Test that the warning goes away when it's fixed
    with ev.Environment(tmp_path):
        output = env("status")
        assert "includes out of date packages or repos" not in output


def test_env_activate_broken_view(
    mutable_mock_env_path, mock_archive, mock_fetch, mock_custom_repository, install_mockery
):
    with ev.create("test"):
        install("--add", "--fake", "trivial-install-test-package")

    # switch to a new repo that doesn't include the installed package
    # test that Spack detects the missing package and fails gracefully
    with spack.repo.use_repositories(mock_custom_repository):
        wrong_repo = env("activate", "--sh", "test")
        assert "Warning: could not load runtime environment" in wrong_repo
        assert "Unknown namespace: builtin_mock" in wrong_repo

    # test replacing repo fixes it
    normal_repo = env("activate", "--sh", "test")
    assert "Warning: could not load runtime environment" not in normal_repo
    assert "Unknown namespace: builtin_mock" not in normal_repo


def test_to_lockfile_dict():
    e = ev.create("test")
    e.add("mpileaks")
    e.concretize()
    context_dict = e._to_lockfile_dict()

    e_copy = ev.create("test_copy")

    e_copy._read_lockfile_dict(context_dict)
    assert e.specs_by_hash == e_copy.specs_by_hash


def test_env_repo():
    e = ev.create("test")
    e.add("mpileaks")
    e.write()

    with ev.read("test"):
        concretize()

    pkg_cls = e.repo.get_pkg_class("mpileaks")
    assert pkg_cls.name == "mpileaks"
    assert pkg_cls.namespace == "builtin_mock"


def test_user_removed_spec(environment_from_manifest):
    """Ensure a user can remove from any position in the spack.yaml file."""
    before = environment_from_manifest(
        """\
spack:
  specs:
  - mpileaks
  - hypre
  - libelf
"""
    )
    before.concretize()
    before.write()

    # user modifies yaml externally to spack and removes hypre
    with open(before.manifest_path, "w", encoding="utf-8") as f:
        f.write(
            """\
spack:
  specs:
  - mpileaks
  - libelf
"""
        )

    after = ev.read("test")
    after.concretize()
    after.write()

    read = ev.read("test")
    assert not any(x.name == "hypre" for x in read.all_specs_generator())


def test_lockfile_spliced_specs(environment_from_manifest, install_mockery):
    """Test that an environment can round-trip a spliced spec."""
    # Create a local install for zmpi to splice in
    # Default concretization is not using zmpi
    zmpi = spack.concretize.concretize_one("zmpi")
    PackageInstaller([zmpi.package], fake=True).install()

    e1 = environment_from_manifest(
        f"""
spack:
  specs:
  - mpileaks
  concretizer:
    splice:
      explicit:
      - target: mpi
        replacement: zmpi/{zmpi.dag_hash()}
"""
    )
    with e1:
        e1.concretize()
        e1.write()

    # By reading into a second environment, we force a round trip to json
    e2 = _env_create("test2", init_file=e1.lock_path)

    # The one spec is mpileaks
    for _, spec in e2.concretized_specs():
        assert spec.spliced
        assert spec["mpi"].satisfies(f"zmpi@{zmpi.version}")
        assert spec["mpi"].build_spec.satisfies(zmpi)


def test_init_from_lockfile(environment_from_manifest):
    """Test that an environment can be instantiated from a lockfile."""
    e1 = environment_from_manifest(
        """
spack:
  specs:
  - mpileaks
  - hypre
  - libelf
"""
    )
    e1.concretize()
    e1.write()

    e2 = _env_create("test2", init_file=e1.lock_path)

    for s1, s2 in zip(e1.user_specs, e2.user_specs):
        assert s1 == s2

    for r1, r2 in zip(e1.concretized_roots, e2.concretized_roots):
        assert r1 == r2

    assert e1.specs_by_hash == e2.specs_by_hash


def test_init_from_yaml(environment_from_manifest):
    """Test that an environment can be instantiated from a lockfile."""
    e1 = environment_from_manifest(
        """
spack:
  specs:
  - mpileaks
  - hypre
  - libelf
"""
    )
    e1.concretize()
    e1.write()

    e2 = _env_create("test2", init_file=e1.manifest_path)

    for s1, s2 in zip(e1.user_specs, e2.user_specs):
        assert s1 == s2

    assert not e2.concretized_roots
    assert not e2.specs_by_hash


@pytest.mark.parametrize("use_name", (True, False))
def test_init_from_env(use_name, environment_from_manifest):
    """Test that an environment can be instantiated from an environment dir"""
    e1 = environment_from_manifest(
        """
spack:
  specs:
  - mpileaks
  - hypre
  - libelf
"""
    )

    with e1:
        # Test that relative paths in the env are not rewritten
        # Test that relative paths outside the env are
        dev_config = {
            "libelf": {"spec": "libelf", "path": "./libelf"},
            "mpileaks": {"spec": "mpileaks", "path": "../mpileaks"},
        }
        spack.config.set("develop", dev_config)
        fs.touch(os.path.join(e1.path, "libelf"))

    e1.concretize()
    e1.write()

    e2 = _env_create("test2", init_file="test" if use_name else e1.path)

    for s1, s2 in zip(e1.user_specs, e2.user_specs):
        assert s1 == s2

    assert e2.concretized_roots == e1.concretized_roots
    assert e2.specs_by_hash == e1.specs_by_hash

    assert os.path.exists(os.path.join(e2.path, "libelf"))
    with e2:
        assert e2.dev_specs["libelf"]["path"] == "./libelf"
        assert e2.dev_specs["mpileaks"]["path"] == os.path.join(
            os.path.dirname(e1.path), "mpileaks"
        )


def test_init_from_env_no_spackfile(tmp_path):
    with pytest.raises(ev.SpackEnvironmentError, match="not a valid environment"):
        _env_create("test", init_file=str(tmp_path))


def test_init_from_yaml_relative_includes(tmp_path: pathlib.Path):
    files = [
        "relative_copied/packages.yaml",
        "./relative_copied/compilers.yaml",
        "repos.yaml",
        "./config.yaml",
    ]

    manifest = f"""
spack:
  specs: []
  include: {files}
"""

    e1_path = tmp_path / "e1"
    e1_manifest = e1_path / "spack.yaml"
    e1_path.mkdir(parents=True, exist_ok=True)
    with open(e1_manifest, "w", encoding="utf-8") as f:
        f.write(manifest)

    for f in files:
        (e1_path / f).parent.mkdir(parents=True, exist_ok=True)
        (e1_path / f).touch()

    e2 = _env_create("test2", init_file=str(e1_manifest))

    for f in files:
        assert os.path.exists(os.path.join(e2.path, f))


# TODO: Should we be supporting relative path rewrites when creating new env from existing?
# TODO: If so, then this should confirm that the absolute include paths in the new env exist.
def test_init_from_yaml_relative_includes_outside_env(tmp_path: pathlib.Path):
    """Ensure relative includes to files outside the environment fail."""
    files = ["../outside_env/repos.yaml"]

    manifest = f"""
spack:
  specs: []
  include:
  - path: {files[0]}
"""

    # subdir to ensure parent of environment dir is not shared
    e1_path = tmp_path / "e1_subdir" / "e1"
    e1_manifest = e1_path / "spack.yaml"
    e1_path.mkdir(parents=True, exist_ok=True)
    with open(e1_manifest, "w", encoding="utf-8") as f:
        f.write(manifest)

    for f in files:
        file_path = e1_path / f
        file_path.parent.mkdir(parents=True, exist_ok=True)
        file_path.touch()

    with pytest.raises(ValueError, match="does not exist"):
        _ = _env_create("test2", init_file=str(e1_manifest))


def test_env_view_external_prefix(tmp_path: pathlib.Path, mutable_database, mock_packages):
    fake_prefix = tmp_path / "a-prefix"
    fake_bin = fake_prefix / "bin"
    fake_bin.mkdir(parents=True, exist_ok=False)

    manifest_dir = tmp_path / "environment"
    manifest_dir.mkdir(parents=True, exist_ok=False)
    manifest_file = manifest_dir / ev.manifest_name
    manifest_file.write_text(
        """\
spack:
  specs:
  - pkg-a
  view: true
"""
    )

    external_config = io.StringIO(
        """\
packages:
  pkg-a:
    externals:
    - spec: pkg-a@2.0
      prefix: {a_prefix}
    buildable: false
""".format(a_prefix=str(fake_prefix))
    )
    external_config_dict = spack.util.spack_yaml.load_config(external_config)

    test_scope = spack.config.InternalConfigScope("env-external-test", data=external_config_dict)
    with spack.config.override(test_scope):
        e = ev.create("test", manifest_file)
        e.concretize()
        # Note: normally installing specs in a test environment requires doing
        # a fake install, but not for external specs since no actions are
        # taken to install them. The installation commands also include
        # post-installation functions like DB-registration, so are important
        # to do (otherwise the package is not considered installed).
        e.install_all()
        e.write()

        env_mod = spack.util.environment.EnvironmentModifications()
        e.add_view_to_env(env_mod, "default")
        env_variables: Dict[str, str] = {}
        env_mod.apply_modifications(env_variables)
        assert str(fake_bin) in env_variables["PATH"]


def test_init_with_file_and_remove(tmp_path: pathlib.Path, monkeypatch):
    """Ensure a user can remove from any position in the spack.yaml file."""
    path = tmp_path / "spack.yaml"

    with fs.working_dir(str(tmp_path)):
        with open(str(path), "w", encoding="utf-8") as f:
            f.write(
                """\
spack:
  specs:
  - mpileaks
"""
            )

        env("create", "test", "spack.yaml")

    out = env("list")
    assert "test" in out

    with ev.read("test"):
        assert "mpileaks" in find()

    env("remove", "-y", "test")

    out = env("list")
    assert "test" not in out


def test_env_with_config(environment_from_manifest):
    e = environment_from_manifest(
        """
spack:
  specs:
  - mpileaks
  packages:
    mpileaks:
      version: ["2.2"]
"""
    )
    with e:
        e.concretize()

    mpileaks_hash = next(x.hash for x in e.concretized_roots if x.root == Spec("mpileaks"))
    mpileaks = e.specs_by_hash[mpileaks_hash]
    assert mpileaks.satisfies("mpileaks@2.2")


def test_with_config_bad_include_create(environment_from_manifest):
    """Confirm missing required include raises expected exception."""
    err = "does not exist"
    with pytest.raises(ValueError, match=err):
        environment_from_manifest(
            """
spack:
  include:
  - /no/such/directory
"""
        )


def test_with_config_bad_include_activate(environment_from_manifest, tmp_path: pathlib.Path):
    env_root = tmp_path / "env-root"
    env_root.mkdir()
    include1 = env_root / "include1.yaml"
    include1.touch()

    spack_yaml = env_root / ev.manifest_name
    spack_yaml.write_text(
        """
spack:
  include:
  - ./include1.yaml
"""
    )

    with ev.Environment(env_root) as e:
        e.concretize()

    # We've created an environment with included config file (which does
    # exist). Now we remove it and check that we get a sensible error.

    os.remove(include1)
    with pytest.raises(ValueError, match="does not exist"):
        ev.activate(ev.Environment(env_root))

    assert ev.active_environment() is None


def test_env_with_include_config_files_same_basename(
    tmp_path: pathlib.Path, environment_from_manifest
):
    file1 = tmp_path / "path" / "to" / "included-config.yaml"
    file1.parent.mkdir(parents=True, exist_ok=True)
    with open(file1, "w", encoding="utf-8") as f:
        f.write(
            """\
        packages:
          libelf:
              version: ["0.8.10"]
        """
        )

    file2 = tmp_path / "second" / "path" / "included-config.yaml"
    file2.parent.mkdir(parents=True, exist_ok=True)
    with open(file2, "w", encoding="utf-8") as f:
        f.write(
            """\
        packages:
          mpileaks:
              version: ["2.2"]
        """
        )

    e = environment_from_manifest(
        f"""
spack:
  include:
  - {file1}
  - {file2}
  specs:
  - libelf
  - mpileaks
"""
    )

    with e:
        e.concretize()

    mpileaks_hash = next(x.hash for x in e.concretized_roots if x.root == Spec("mpileaks"))
    mpileaks = e.specs_by_hash[mpileaks_hash]
    assert mpileaks.satisfies("mpileaks@2.2")

    libelf_hash = next(x.hash for x in e.concretized_roots if x.root == Spec("libelf"))
    libelf = e.specs_by_hash[libelf_hash]
    assert libelf.satisfies("libelf@0.8.10")


@pytest.fixture(scope="function")
def packages_file(tmp_path: pathlib.Path):
    """Return the path to the packages configuration file."""
    raw_yaml = """
packages:
  mpileaks:
    version: ["2.2"]
"""
    config_dir = tmp_path / "testconfig"
    config_dir.mkdir()
    filename = config_dir / "packages.yaml"
    filename.write_text(raw_yaml)
    yield filename


def mpileaks_env_config(include_path):
    """Return the contents of an environment that includes the provided
    path and lists mpileaks as the sole spec."""
    return """\
spack:
  include:
  - {0}
  specs:
  - mpileaks
""".format(include_path)


def test_env_with_included_config_file(mutable_mock_env_path, packages_file):
    """Test inclusion of a relative packages configuration file added to an
    existing environment.
    """
    env_root = mutable_mock_env_path
    env_root.mkdir(parents=True, exist_ok=True)
    include_filename = "included-config.yaml"
    included_path = env_root / include_filename
    shutil.move(str(packages_file), included_path)

    spack_yaml = env_root / ev.manifest_name
    spack_yaml.write_text(
        f"""\
spack:
  include:
  - {os.path.join(".", include_filename)}
  specs:
  - mpileaks
"""
    )

    e = ev.Environment(env_root)
    with e:
        e.concretize()

    mpileaks_hash = next(x.hash for x in e.concretized_roots if x.root == Spec("mpileaks"))
    mpileaks = e.specs_by_hash[mpileaks_hash]
    assert mpileaks.satisfies("mpileaks@2.2")


def test_config_change_existing(
    mutable_mock_env_path, tmp_path: pathlib.Path, mock_packages, mutable_config
):
    """Test ``config change`` with config in the ``spack.yaml`` as well as an
    included file scope.
    """

    env_path = tmp_path / "test_config"
    env_path.mkdir(parents=True, exist_ok=True)
    included_file = "included-packages.yaml"
    included_path = env_path / included_file
    with open(included_path, "w", encoding="utf-8") as f:
        f.write(
            """\
packages:
  mpich:
    require:
    - spec: "@3.0.2"
  libelf:
    require: "@0.8.10"
  bowtie:
    require:
    - one_of: ["@1.3.0", "@1.2.0"]
"""
        )

    spack_yaml = env_path / ev.manifest_name
    spack_yaml.write_text(
        f"""\
spack:
  packages:
    mpich:
      require:
      - spec: "+debug"
  include:
  - {os.path.join(".", included_file)}
  specs: []
"""
    )

    mutable_config.set("config:misc_cache", str(tmp_path / "cache"))
    e = ev.Environment(env_path)
    with e:
        # List of requirements, flip a variant
        config("change", "packages:mpich:require:~debug")
        test_spec = spack.concretize.concretize_one("mpich")
        assert test_spec.satisfies("@3.0.2~debug")

        # List of requirements, change the version (in a different scope)
        config("change", "packages:mpich:require:@3.0.3")
        test_spec = spack.concretize.concretize_one("mpich")
        assert test_spec.satisfies("@3.0.3")

        # "require:" as a single string, also try specifying
        # a spec string that requires enclosing in quotes as
        # part of the config path
        config("change", 'packages:libelf:require:"@0.8.12:"')
        spack.concretize.concretize_one("libelf@0.8.12")
        # No need for assert, if there wasn't a failure, we
        # changed the requirement successfully.

        # Use change to add a requirement for a package that
        # has no requirements defined
        config("change", "packages:fftw:require:+mpi")
        test_spec = spack.concretize.concretize_one("fftw")
        assert test_spec.satisfies("+mpi")
        config("change", "packages:fftw:require:~mpi")
        test_spec = spack.concretize.concretize_one("fftw")
        assert test_spec.satisfies("~mpi")
        config("change", "packages:fftw:require:@1.0")
        test_spec = spack.concretize.concretize_one("fftw")
        assert test_spec.satisfies("@1.0~mpi")

        # Use "--match-spec" to change one spec in a "one_of"
        # list
        config("change", "packages:bowtie:require:@1.2.2", "--match-spec", "@1.2.0")
        # confirm that we can concretize to either value
        spack.concretize.concretize_one("bowtie@1.3.0")
        spack.concretize.concretize_one("bowtie@1.2.2")
        # confirm that we cannot concretize to the old value
        with pytest.raises(spack.solver.asp.UnsatisfiableSpecError):
            spack.concretize.concretize_one("bowtie@1.2.0")


def test_config_change_new(
    mutable_mock_env_path, tmp_path: pathlib.Path, mock_packages, mutable_config
):
    spack_yaml = tmp_path / ev.manifest_name
    spack_yaml.write_text(
        """\
spack:
  specs: []
"""
    )

    with ev.Environment(tmp_path):
        config("change", "packages:mpich:require:~debug")
        with pytest.raises(spack.solver.asp.UnsatisfiableSpecError):
            spack.concretize.concretize_one("mpich+debug")
        spack.concretize.concretize_one("mpich~debug")

    # Now check that we raise an error if we need to add a require: constraint
    # when preexisting config manually specified it as a singular spec
    spack_yaml.write_text(
        """\
spack:
  specs: []
  packages:
    mpich:
      require: "@3.0.3"
"""
    )
    with ev.Environment(tmp_path):
        assert spack.concretize.concretize_one("mpich").satisfies("@3.0.3")
        with pytest.raises(spack.error.ConfigError, match="not a list"):
            config("change", "packages:mpich:require:~debug")


def test_env_with_included_config_file_url(
    tmp_path: pathlib.Path, mutable_empty_config, packages_file
):
    """Test configuration inclusion of a file whose path is a URL before
    the environment is concretized."""

    spack_yaml = tmp_path / "spack.yaml"
    with spack_yaml.open("w") as f:
        f.write("spack:\n  include:\n    - {0}\n".format(packages_file.as_uri()))

    env = ev.Environment(str(tmp_path))
    ev.activate(env)

    cfg = spack.config.get("packages")
    assert cfg["mpileaks"]["version"] == ["2.2"]


def test_env_with_included_config_scope(mutable_mock_env_path, packages_file):
    """Test inclusion of a package file from the environment's configuration
    stage directory. This test is intended to represent a case where a remote
    file has already been staged."""
    env_root = mutable_mock_env_path
    config_scope_path = env_root / "config"

    # Copy the packages.yaml file to the environment configuration
    # directory, so it is picked up during concretization. (Using
    # copy instead of rename in case the fixture scope changes.)
    config_scope_path.mkdir(parents=True, exist_ok=True)
    include_filename = packages_file.name
    included_path = config_scope_path / include_filename
    fs.copy(str(packages_file), included_path)

    # Configure the environment to include file(s) from the environment's
    # remote configuration stage directory.
    spack_yaml = env_root / ev.manifest_name
    spack_yaml.write_text(mpileaks_env_config(config_scope_path))

    # Ensure the concretized environment reflects contents of the
    # packages.yaml file.
    e = ev.Environment(env_root)
    with e:
        e.concretize()

    mpileaks_hash = next(x.hash for x in e.concretized_roots if x.root == Spec("mpileaks"))
    mpileaks = e.specs_by_hash[mpileaks_hash]
    assert mpileaks.satisfies("mpileaks@2.2")


def test_env_with_included_config_var_path(tmp_path: pathlib.Path, packages_file):
    """Test inclusion of a package configuration file with path variables
    "staged" in the environment's configuration stage directory."""
    included_file = str(packages_file)
    env_path = tmp_path
    config_var_path = os.path.join("$tempdir", "included-packages.yaml")

    spack_yaml = env_path / ev.manifest_name
    spack_yaml.write_text(mpileaks_env_config(config_var_path))

    config_real_path = substitute_path_variables(config_var_path)
    shutil.move(included_file, config_real_path)
    assert os.path.exists(config_real_path)

    e = ev.Environment(env_path)
    with e:
        e.concretize()

    mpileaks_hash = next(x.hash for x in e.concretized_roots if x.root == Spec("mpileaks"))
    mpileaks = e.specs_by_hash[mpileaks_hash]
    assert mpileaks.satisfies("mpileaks@2.2")


def test_env_with_included_config_precedence(tmp_path: pathlib.Path):
    """Test included scope and manifest precedence when including a package
    configuration file."""

    included_file = "included-packages.yaml"
    included_path = tmp_path / included_file
    with open(included_path, "w", encoding="utf-8") as f:
        f.write(
            """\
packages:
  mpileaks:
    version: ["2.2"]
  libelf:
    version: ["0.8.10"]
"""
        )

    spack_yaml = tmp_path / ev.manifest_name
    spack_yaml.write_text(
        f"""\
spack:
  packages:
    libelf:
      version: ["0.8.12"]
  include:
  - {os.path.join(".", included_file)}
  specs:
  - mpileaks
"""
    )

    e = ev.Environment(tmp_path)
    with e:
        e.concretize()

    mpileaks_hash = next(x.hash for x in e.concretized_roots if x.root == Spec("mpileaks"))
    mpileaks = e.specs_by_hash[mpileaks_hash]

    # ensure included scope took effect
    assert mpileaks.satisfies("mpileaks@2.2")

    # ensure env file takes precedence
    assert mpileaks["libelf"].satisfies("libelf@0.8.12")


def test_env_with_included_configs_precedence(tmp_path: pathlib.Path):
    """Test precedence of multiple included configuration files."""
    file1 = "high-config.yaml"
    file2 = "low-config.yaml"

    spack_yaml = tmp_path / ev.manifest_name
    spack_yaml.write_text(
        f"""\
spack:
  include:
  - {os.path.join(".", file1)} # this one should take precedence
  - {os.path.join(".", file2)}
  specs:
  - mpileaks
"""
    )

    with open(tmp_path / file1, "w", encoding="utf-8") as f:
        f.write(
            """\
packages:
  libelf:
    version: ["0.8.10"]  # this should override libelf version below
"""
        )

    with open(tmp_path / file2, "w", encoding="utf-8") as f:
        f.write(
            """\
packages:
  mpileaks:
    version: ["2.2"]
  libelf:
    version: ["0.8.12"]
"""
        )

    e = ev.Environment(tmp_path)
    with e:
        e.concretize()

    mpileaks_hash = next(x.hash for x in e.concretized_roots if x.root == Spec("mpileaks"))
    mpileaks = e.specs_by_hash[mpileaks_hash]

    # ensure the included package spec took precedence over manifest spec
    assert mpileaks.satisfies("mpileaks@2.2")

    # ensure the first included package spec took precedence over one from second
    assert mpileaks["libelf"].satisfies("libelf@0.8.10")


@pytest.mark.regression("39248")
def test_bad_env_yaml_format_remove(mutable_mock_env_path):
    badenv = "badenv"
    env("create", badenv)
    filename = mutable_mock_env_path / "spack.yaml"
    with open(filename, "w", encoding="utf-8") as f:
        f.write(
            """\
    - mpileaks
"""
        )

    assert badenv in env("list")
    env("remove", "-y", badenv)
    assert badenv not in env("list")


@pytest.mark.regression("39248")
@pytest.mark.parametrize(
    "error,message,contents",
    [
        (
            spack.config.ConfigFormatError,
            "not of type",
            """\
spack:
  specs: mpi@2.0
""",
        ),
        (
            ev.SpackEnvironmentConfigError,
            "duplicate key",
            """\
spack:
  packages:
    all:
      providers:
        mpi: [mvapich2]
        mpi: [mpich]
""",
        ),
        (
            spack.config.ConfigFormatError,
            "'specks' was unexpected",
            """\
spack:
  specks:
    - libdwarf
""",
        ),
    ],
)
def test_bad_env_yaml_create_fails(
    tmp_path: pathlib.Path, mutable_mock_env_path, error, message, contents
):
    """Ensure creation with invalid yaml does NOT create or leave the environment."""
    filename = tmp_path / ev.manifest_name
    filename.write_text(contents)
    env_name = "bad_env"
    with pytest.raises(error, match=message):
        env("create", env_name, str(filename))

    assert env_name not in env("list")
    manifest = mutable_mock_env_path / env_name / ev.manifest_name
    assert not os.path.exists(str(manifest))


@pytest.mark.regression("39248")
@pytest.mark.parametrize("answer", ["-y", ""])
def test_multi_env_remove(mutable_mock_env_path, monkeypatch, answer):
    """Test removal (or not) of a valid and invalid environment"""
    remove_environment = answer == "-y"
    monkeypatch.setattr(tty, "get_yes_or_no", lambda prompt, default: remove_environment)

    environments = ["goodenv", "badenv"]
    for e in environments:
        env("create", e)

    # Ensure the bad environment contains invalid yaml
    filename = mutable_mock_env_path / environments[1] / ev.manifest_name
    filename.write_text(
        """\
    - libdwarf
"""
    )

    assert all(e in env("list") for e in environments)

    args = [answer] if answer else []
    args.extend(environments)
    output = env("remove", *args, fail_on_error=False)

    if remove_environment is True:
        # Successfully removed (and reported removal) of *both* environments
        assert not all(e in env("list") for e in environments)
        assert output.count("Successfully removed") == len(environments)
    else:
        # Not removing any of the environments
        assert all(e in env("list") for e in environments)


def test_env_loads(install_mockery, mock_fetch, mock_modules_root):
    env("create", "test")

    with ev.read("test"):
        add("mpileaks")
        concretize()
        install("--fake")
        module("tcl", "refresh", "-y")

    with ev.read("test"):
        env("loads")

    e = ev.read("test")

    loads_file = os.path.join(e.path, "loads")
    assert os.path.exists(loads_file)

    with open(loads_file, encoding="utf-8") as f:
        contents = f.read()
        assert "module load mpileaks" in contents


@pytest.mark.disable_clean_stage_check
def test_stage(mock_stage, mock_fetch, install_mockery):
    env("create", "test")
    with ev.read("test"):
        add("mpileaks")
        add("zmpi")
        concretize()
        stage()

    root = str(mock_stage)

    def check_stage(spec):
        spec = spack.concretize.concretize_one(spec)
        for dep in spec.traverse():
            stage_name = f"{stage_prefix}{dep.name}-{dep.version}-{dep.dag_hash()}"
            if dep.external:
                assert not os.path.exists(os.path.join(root, stage_name))
            else:
                assert os.path.isdir(os.path.join(root, stage_name))

    check_stage("mpileaks")
    check_stage("zmpi")


def test_env_commands_die_with_no_env_arg():
    # these fail in argparse when given no arg
    with pytest.raises(SpackCommandError):
        env("create")
    with pytest.raises(SpackCommandError):
        env("remove")

    # these have an optional env arg and raise errors via tty.die
    with pytest.raises(SpackCommandError):
        env("loads")

    # This should NOT raise an error with no environment
    # it just tells the user there isn't an environment
    env("status")


def test_env_blocks_uninstall(mock_stage, mock_fetch, install_mockery):
    env("create", "test")
    with ev.read("test"):
        add("mpileaks")
        install("--fake")

    out = uninstall("-y", "mpileaks", fail_on_error=False)
    assert uninstall.returncode == 1
    assert "The following environments still reference these specs" in out


def test_roots_display_with_variants():
    env("create", "test")
    with ev.read("test"):
        add("boost+shared")

    with ev.read("test"):
        out = find()

    assert "boost+shared" in out


def test_uninstall_keeps_in_env(mock_stage, mock_fetch, install_mockery):
    # 'spack uninstall' without --remove should not change the environment
    # spack.yaml file, just uninstall specs
    env("create", "test")
    with ev.read("test"):
        add("mpileaks")
        add("libelf")
        install("--fake")

    test = ev.read("test")
    # Save this spec to check later if it is still in the env
    (mpileaks_hash,) = list(x for x, y in test.specs_by_hash.items() if y.name == "mpileaks")
    user_specs_before = test.user_specs
    user_spec_hashes_before = {x.hash for x in test.concretized_roots}

    with ev.read("test"):
        uninstall("-ya")

    test = ev.read("test")
    assert {x.hash for x in test.concretized_roots} == user_spec_hashes_before
    assert test.user_specs.specs == user_specs_before.specs
    assert mpileaks_hash in test.specs_by_hash
    assert not test.specs_by_hash[mpileaks_hash].installed


def test_uninstall_removes_from_env(mock_stage, mock_fetch, install_mockery):
    # 'spack uninstall --remove' should update the environment
    env("create", "test")
    with ev.read("test"):
        add("mpileaks")
        add("libelf")
        install("--fake")

    with ev.read("test"):
        uninstall("-y", "-a", "--remove")

    test = ev.read("test")
    assert not test.specs_by_hash
    assert not test.concretized_roots
    assert not test.user_specs


def test_indirect_build_dep(repo_builder: RepoBuilder):
    """Simple case of X->Y->Z where Y is a build/link dep and Z is a
    build-only dep. Make sure this concrete DAG is preserved when writing the
    environment out and reading it back.
    """
    repo_builder.add_package("z")
    repo_builder.add_package("y", dependencies=[("z", "build", None)])
    repo_builder.add_package("x", dependencies=[("y", None, None)])

    with spack.repo.use_repositories(repo_builder.root):
        x_spec = Spec("x")
        x_concretized = spack.concretize.concretize_one(x_spec)

        _env_create("test", with_view=False)
        e = ev.read("test")
        e.add(x_spec)
        e.concretize()
        e.write()

        e_read = ev.read("test")
        assert len(e_read.concretized_roots) == 1
        x_env_hash = e_read.concretized_roots[0].hash
        x_env_spec = e_read.specs_by_hash[x_env_hash]
        assert x_env_spec == x_concretized


def test_store_different_build_deps(repo_builder: RepoBuilder):
    r"""Ensure that an environment can store two instances of a build-only
    dependency::

              x       y
             /| (l)   | (b)
        (b) | y       z2
             \| (b)
              z1

    """
    repo_builder.add_package("z")
    repo_builder.add_package("y", dependencies=[("z", "build", None)])
    repo_builder.add_package("x", dependencies=[("y", None, None), ("z", "build", None)])

    with spack.repo.use_repositories(repo_builder.root):
        y_spec = Spec("y ^z@3")
        y_concretized = spack.concretize.concretize_one(y_spec)

        x_spec = Spec("x ^z@2")
        x_concretized = spack.concretize.concretize_one(x_spec)

        # Even though x chose a different 'z', the y it chooses should be identical
        # *aside* from the dependency on 'z'.  The dag_hash() will show the difference
        # in build dependencies.
        assert x_concretized["y"].eq_node(y_concretized)
        assert x_concretized["y"].dag_hash() != y_concretized.dag_hash()

        _env_create("test", with_view=False)
        e = ev.read("test")
        e.add(y_spec)
        e.add(x_spec)
        e.concretize()
        e.write()

        e_read = ev.read("test")
        y_env_hash, x_env_hash = [x.hash for x in e_read.concretized_roots]

        y_read = e_read.specs_by_hash[y_env_hash]
        x_read = e_read.specs_by_hash[x_env_hash]

        # make sure the DAG hashes and build deps are preserved after
        # a round trip to/from the lockfile
        assert x_read["z"] != y_read["z"]
        assert x_read["z"].dag_hash() != y_read["z"].dag_hash()

        assert x_read["y"].eq_node(y_read)
        assert x_read["y"].dag_hash() != y_read.dag_hash()


def test_env_updates_view_install(tmp_path: pathlib.Path, mock_stage, mock_fetch, install_mockery):
    view_dir = tmp_path / "view"
    env("create", "--with-view=%s" % view_dir, "test")
    with ev.read("test"):
        add("mpileaks")
        install("--fake")

    check_mpileaks_and_deps_in_view(view_dir)


def test_env_view_fails(
    tmp_path: pathlib.Path, mock_packages, mock_stage, mock_fetch, install_mockery
):
    # We currently ignore file-file conflicts for the prefix merge,
    # so in principle there will be no errors in this test. But
    # the .spack metadata dir is handled separately and is more strict.
    # It also throws on file-file conflicts. That's what we're checking here
    # by adding the same package twice to a view.
    view_dir = tmp_path / "view"
    env("create", "--with-view=%s" % view_dir, "test")
    with ev.read("test"):
        add("libelf")
        add("libelf cflags=-g")
        with pytest.raises(
            ev.SpackEnvironmentViewError, match="two specs project to the same prefix"
        ):
            install("--fake")


def test_env_view_fails_dir_file(
    tmp_path: pathlib.Path, mock_packages, mock_stage, mock_fetch, install_mockery
):
    # This environment view fails to be created because a file
    # and a dir are in the same path. Test that it mentions the problematic path.
    view_dir = tmp_path / "view"
    env("create", "--with-view=%s" % view_dir, "test")
    with ev.read("test"):
        add("view-file")
        add("view-dir")
        with pytest.raises(
            spack.llnl.util.link_tree.MergeConflictSummary, match=os.path.join("bin", "x")
        ):
            install()


def test_env_view_succeeds_symlinked_dir_file(
    tmp_path: pathlib.Path, mock_packages, mock_stage, mock_fetch, install_mockery
):
    # A symlinked dir and an ordinary dir merge happily
    view_dir = tmp_path / "view"
    env("create", "--with-view=%s" % view_dir, "test")
    with ev.read("test"):
        add("view-symlinked-dir")
        add("view-dir")
        install()
        x_dir = os.path.join(str(view_dir), "bin", "x")
        assert os.path.exists(os.path.join(x_dir, "file_in_dir"))
        assert os.path.exists(os.path.join(x_dir, "file_in_symlinked_dir"))


def test_env_without_view_install(tmp_path: pathlib.Path, mock_stage, mock_fetch, install_mockery):
    # Test enabling a view after installing specs
    env("create", "--without-view", "test")

    test_env = ev.read("test")
    with pytest.raises(ev.SpackEnvironmentError):
        test_env.default_view

    view_dir = tmp_path / "view"

    with ev.read("test"):
        add("mpileaks")
        install("--fake")

        env("view", "enable", str(view_dir))

    # After enabling the view, the specs should be linked into the environment
    # view dir
    check_mpileaks_and_deps_in_view(view_dir)


@pytest.mark.parametrize("env_name", [True, False])
def test_env_include_concrete_env_yaml(env_name):
    env("create", "test")
    test = ev.read("test")

    with test:
        add("mpileaks")
    test.concretize()
    test.write()

    environ = "test" if env_name else test.path

    env("create", "--include-concrete", environ, "combined_env")

    combined = ev.read("combined_env")
    combined_yaml = combined.manifest["spack"]

    assert ev.lockfile_include_key in combined_yaml
    assert test.path in combined_yaml[ev.lockfile_include_key]


@pytest.mark.regression("45766")
@pytest.mark.parametrize("format", ["v1", "v2", "v3"])
def test_env_include_concrete_old_env(format):
    lockfile = os.path.join(spack.paths.test_path, "data", "legacy_env", f"{format}.lock")
    # create an env from old .lock file -- this does not update the format
    env("create", "old-env", lockfile)
    env("create", "--include-concrete", "old-env", "test")

    assert ev.read("old-env").all_specs() == ev.read("test").all_specs()


def test_env_bad_include_concrete_env():
    with pytest.raises(ev.SpackEnvironmentError):
        env("create", "--include-concrete", "nonexistent_env", "combined_env")


def test_env_not_concrete_include_concrete_env():
    env("create", "test")
    test = ev.read("test")

    with test:
        add("mpileaks")

    with pytest.raises(ev.SpackEnvironmentError):
        env("create", "--include-concrete", "test", "combined_env")


def test_env_multiple_include_concrete_envs():
    test1, test2, combined = setup_combined_multiple_env()

    combined_yaml = combined.manifest["spack"]

    assert test1.path in combined_yaml[ev.lockfile_include_key][0]
    assert test2.path in combined_yaml[ev.lockfile_include_key][1]

    # No local specs in the combined env
    assert not combined_yaml["specs"]


def test_env_include_concrete_envs_lockfile():
    test1, test2, combined = setup_combined_multiple_env()

    combined_yaml = combined.manifest["spack"]

    assert ev.lockfile_include_key in combined_yaml
    assert test1.path in combined_yaml[ev.lockfile_include_key]

    with open(combined.lock_path, encoding="utf-8") as f:
        lockfile_as_dict = combined._read_lockfile(f)

    assert set(
        entry["hash"] for entry in lockfile_as_dict[ev.lockfile_include_key][test1.path]["roots"]
    ) == set(test1.specs_by_hash)
    assert set(
        entry["hash"] for entry in lockfile_as_dict[ev.lockfile_include_key][test2.path]["roots"]
    ) == set(test2.specs_by_hash)


def test_env_include_concrete_add_env():
    test1, test2, combined = setup_combined_multiple_env()

    # create new env & concretize
    env("create", "new")
    new_env = ev.read("new")
    with new_env:
        add("mpileaks")

    new_env.concretize()
    new_env.write()

    # add new env to combined
    combined.included_concrete_env_root_dirs.append(new_env.path)

    # assert thing haven't changed yet
    with open(combined.lock_path, encoding="utf-8") as f:
        lockfile_as_dict = combined._read_lockfile(f)

    assert new_env.path not in lockfile_as_dict[ev.lockfile_include_key].keys()

    # concretize combined env with new env
    combined.concretize()
    combined.write()

    # assert changes
    with open(combined.lock_path, encoding="utf-8") as f:
        lockfile_as_dict = combined._read_lockfile(f)

    assert new_env.path in lockfile_as_dict[ev.lockfile_include_key].keys()


def test_env_include_concrete_remove_env():
    test1, test2, combined = setup_combined_multiple_env()

    # remove test2 from combined
    combined.included_concrete_env_root_dirs = [test1.path]

    # assert test2 is still in combined's lockfile
    with open(combined.lock_path, encoding="utf-8") as f:
        lockfile_as_dict = combined._read_lockfile(f)

    assert test2.path in lockfile_as_dict[ev.lockfile_include_key].keys()

    # reconcretize combined
    combined.concretize()
    combined.write()

    # assert test2 is not in combined's lockfile
    with open(combined.lock_path, encoding="utf-8") as f:
        lockfile_as_dict = combined._read_lockfile(f)

    assert test2.path not in lockfile_as_dict[ev.lockfile_include_key].keys()


def configure_reuse(reuse_mode, combined_env) -> Optional[ev.Environment]:
    override_env = None
    _config: Dict[Any, Any] = {}
    if reuse_mode == "true":
        _config = {"concretizer": {"reuse": True}}
    elif reuse_mode == "from_environment":
        _config = {"concretizer": {"reuse": {"from": [{"type": "environment"}]}}}
    elif reuse_mode == "from_environment_test1":
        _config = {"concretizer": {"reuse": {"from": [{"type": "environment", "path": "test1"}]}}}
    elif reuse_mode == "from_environment_external_test":
        # Create a new environment called external_test that enables the "debug"
        # The default is "~debug"
        env("create", "external_test")
        override_env = ev.read("external_test")
        with override_env:
            add("mpich@1.0 +debug")
        override_env.concretize()
        override_env.write()

        # Reuse from the environment that is not included.
        # Specify the requirement for the debug variant. By default this would concretize to use
        # mpich@3.0 but with include concrete the mpich@1.0 +debug version from the
        # "external_test" environment will be used.
        _config = {
            "concretizer": {"reuse": {"from": [{"type": "environment", "path": "external_test"}]}},
            "packages": {"mpich": {"require": ["+debug"]}},
        }
    elif reuse_mode == "from_environment_raise":
        _config = {
            "concretizer": {"reuse": {"from": [{"type": "environment", "path": "not-a-real-env"}]}}
        }
    # Disable unification in these tests to avoid confusing reuse due to unification using an
    # include concrete spec vs reuse due to the reuse configuration
    _config["concretizer"].update({"unify": False})

    combined_env.manifest.configuration.update(_config)
    combined_env.manifest.changed = True
    combined_env.write()

    return override_env


@pytest.mark.parametrize(
    "reuse_mode",
    [
        "true",
        "from_environment",
        "from_environment_test1",
        "from_environment_external_test",
        "from_environment_raise",
    ],
)
def test_env_include_concrete_reuse(reuse_mode):
    # The default mpi version is 3.x provided by mpich in the mock repo.
    # This test verifies that concretizing with an included concrete
    # environment with "concretizer:reuse:true" the included
    # concrete spec overrides the default with mpi@1.0.
    test1, _, combined = setup_combined_multiple_env()

    # Set the reuse mode for the environment
    override_env = configure_reuse(reuse_mode, combined)
    if override_env:
        # If there is an override environment (ie. testing reuse with
        # an external environment) update it here.
        test1 = override_env

    # Capture the test1 specs included by combined
    test1_specs_by_hash = test1.specs_by_hash

    try:
        # Add mpileaks to the combined environment
        with combined:
            add("mpileaks")
            combined.concretize()
        comb_specs_by_hash = combined.specs_by_hash

        # create reference env with mpileaks that does not use reuse
        # This should concretize to the default version of mpich (3.0)
        env("create", "new")
        ref_env = ev.read("new")
        with ref_env:
            add("mpileaks")
        ref_env.concretize()
        ref_specs_by_hash = ref_env.specs_by_hash

        # Ensure that the mpich used by the mpileaks is the mpich from the reused test environment
        comb_mpileaks_spec = [s for s in comb_specs_by_hash.values() if s.name == "mpileaks"]
        test1_mpich_spec = [s for s in test1_specs_by_hash.values() if s.name == "mpich"]
        assert len(comb_mpileaks_spec) == 1
        assert len(test1_mpich_spec) == 1
        assert comb_mpileaks_spec[0]["mpich"].dag_hash() == test1_mpich_spec[0].dag_hash()

        # None of the references specs (using mpich@3) reuse specs from test1.
        # This tests that the reuse is not happening coincidently
        assert not any([s in test1_specs_by_hash for s in ref_specs_by_hash])

        # Make sure the raise tests raises
        assert "raise" not in reuse_mode
    except ev.SpackEnvironmentError:
        assert "raise" in reuse_mode


@pytest.mark.parametrize("unify", [True, False, "when_possible"])
def test_env_include_concrete_env_reconcretized(mutable_config, unify):
    """Double check to make sure that concrete_specs for the local specs is empty
    after reconcretizing.
    """
    _, _, combined = setup_combined_multiple_env()

    with open(combined.lock_path, encoding="utf-8") as f:
        lockfile_as_dict = combined._read_lockfile(f)

    assert not lockfile_as_dict["roots"]
    assert not lockfile_as_dict["concrete_specs"]

    with combined:
        mutable_config.set("concretizer:unify", unify)
        combined.concretize()
        combined.write()

    with open(combined.lock_path, encoding="utf-8") as f:
        lockfile_as_dict = combined._read_lockfile(f)

    assert not lockfile_as_dict["roots"]
    assert not lockfile_as_dict["concrete_specs"]


def test_concretize_include_concrete_env():
    """Tests that if we update an included environment, and later we re-concretize the environment
    that includes it, we use the latest version of the concrete specs.
    """
    test1, _, combined = setup_combined_multiple_env()

    # Update test1 environment
    with test1:
        add("mpileaks")
    test1.concretize()
    test1.write()

    # Check the test1 environment includes mpileaks, while the combined environment does not
    assert Spec("mpileaks") in {x.root for x in test1.concretized_roots}
    assert Spec("mpileaks") not in {
        x.root for x in combined.included_concretized_roots[test1.path]
    }

    # If we update the combined environment, it will include mpileaks too
    combined.concretize()
    combined.write()
    assert Spec("mpileaks") in {x.root for x in combined.included_concretized_roots[test1.path]}


def test_concretize_nested_include_concrete_envs():
    env("create", "test1")
    test1 = ev.read("test1")
    with test1:
        add("zlib")
    test1.concretize()
    test1.write()

    os.environ["TEST1_ROOT"] = test1.path
    env("create", "--include-concrete", "${TEST1_ROOT}", "test2")
    test2 = ev.read("test2")
    with test2:
        add("libelf")
    test2.concretize()
    test2.write()

    env("create", "--include-concrete", "test2", "test3")
    test3 = ev.read("test3")

    with open(test3.lock_path, encoding="utf-8") as f:
        lockfile_as_dict = test3._read_lockfile(f)

    assert test2.path in lockfile_as_dict[ev.lockfile_include_key]
    assert (
        test1.path
        in lockfile_as_dict[ev.lockfile_include_key][test2.path][ev.lockfile_include_key]
    )

    assert Spec("zlib") in {x.root for x in test3.included_concretized_roots[test1.path]}


def test_concretize_nested_included_concrete():
    """Confirm that nested included environments use specs concretized at
    environment creation time and change with reconcretization."""
    env("create", "test1")
    test1 = ev.read("test1")
    with test1:
        add("zlib")
    test1.concretize()
    test1.write()

    # test2 should include test1 with zlib
    env("create", "--include-concrete", "test1", "test2")
    test2 = ev.read("test2")
    with test2:
        add("libelf")
    test2.concretize()
    test2.write()

    assert Spec("zlib") in {x.root for x in test2.included_concretized_roots[test1.path]}

    # Modify/re-concretize test1 to replace zlib with mpileaks
    with test1:
        remove("zlib")
        add("mpileaks")
    test1.concretize()
    test1.write()

    # test3 should include the latest concretization of test1
    env("create", "--include-concrete", "test1", "test3")
    test3 = ev.read("test3")
    with test3:
        add("callpath")
    test3.concretize()
    test3.write()

    included_roots = test3.included_concretized_roots[test1.path]
    assert len(included_roots) == 1
    assert Spec("mpileaks") in {x.root for x in included_roots}

    # The last concretization of test4's included environments should have test2
    # with the original concretized test1 spec and test3 with the re-concretized
    # test1 spec.
    env("create", "--include-concrete", "test2", "--include-concrete", "test3", "test4")
    test4 = ev.read("test4")

    def included_included_spec(path1, path2):
        included_path1 = test4.included_concrete_spec_data[path1]
        included_path2 = included_path1[ev.lockfile_include_key][path2]
        return included_path2["roots"][0]["spec"]

    included_test2_test1 = included_included_spec(test2.path, test1.path)
    assert "zlib" in included_test2_test1

    included_test3_test1 = included_included_spec(test3.path, test1.path)
    assert "mpileaks" in included_test3_test1

    # test4's concretized specs should reflect the original concretization.
    concrete_specs = [s for s, _ in test4.concretized_specs()]
    expected = [Spec(s) for s in ["libelf", "zlib", "mpileaks", "callpath"]]
    assert all(s in concrete_specs for s in expected)

    # Re-concretize test2 to reflect the new concretization of included test1
    # to remove zlib and write it out so it can be picked up by test4.
    # Re-concretize test4 to reflect the re-concretization of included test2
    # and ensure that its included specs are up-to-date
    test2.concretize()
    test2.write()
    test4.concretize()

    concrete_specs = [s for s, _ in test4.concretized_specs()]
    assert Spec("zlib") not in concrete_specs

    # Expecting mpileaks to appear only once
    expected = [Spec(s) for s in ["libelf", "mpileaks", "callpath"]]
    assert len(concrete_specs) == 3 and all(s in concrete_specs for s in expected)


def test_env_config_view_default(
    environment_from_manifest, mock_stage, mock_fetch, install_mockery
):
    # This config doesn't mention whether a view is enabled
    environment_from_manifest(
        """
spack:
  specs:
  - mpileaks
"""
    )

    with ev.read("test"):
        install("--fake")

    e = ev.read("test")

    # Check that metadata folder for this spec exists
    assert os.path.isdir(os.path.join(e.default_view.view()._root, ".spack", "mpileaks"))


def test_env_updates_view_install_package(
    tmp_path: pathlib.Path, mock_stage, mock_fetch, install_mockery
):
    view_dir = tmp_path / "view"
    env("create", "--with-view=%s" % view_dir, "test")
    with ev.read("test"):
        install("--fake", "--add", "mpileaks")

    assert os.path.exists(str(view_dir / ".spack/mpileaks"))


def test_env_updates_view_add_concretize(
    tmp_path: pathlib.Path, mock_stage, mock_fetch, install_mockery
):
    view_dir = tmp_path / "view"
    env("create", "--with-view=%s" % view_dir, "test")
    install("--fake", "mpileaks")
    with ev.read("test"):
        add("mpileaks")
        concretize()

    check_mpileaks_and_deps_in_view(view_dir)


def test_env_updates_view_uninstall(
    tmp_path: pathlib.Path, mock_stage, mock_fetch, install_mockery
):
    view_dir = tmp_path / "view"
    env("create", "--with-view=%s" % view_dir, "test")
    with ev.read("test"):
        install("--fake", "--add", "mpileaks")

    check_mpileaks_and_deps_in_view(view_dir)

    with ev.read("test"):
        uninstall("-ay")

    check_viewdir_removal(view_dir)


def test_env_updates_view_uninstall_referenced_elsewhere(
    tmp_path: pathlib.Path, mock_stage, mock_fetch, install_mockery
):
    view_dir = tmp_path / "view"
    env("create", "--with-view=%s" % view_dir, "test")
    install("--fake", "mpileaks")
    with ev.read("test"):
        add("mpileaks")
        concretize()

    check_mpileaks_and_deps_in_view(view_dir)

    with ev.read("test"):
        uninstall("-ay")

    check_viewdir_removal(view_dir)


def test_env_updates_view_remove_concretize(
    tmp_path: pathlib.Path, mock_stage, mock_fetch, install_mockery
):
    view_dir = tmp_path / "view"
    env("create", "--with-view=%s" % view_dir, "test")
    install("--fake", "mpileaks")
    with ev.read("test"):
        add("mpileaks")
        concretize()

    check_mpileaks_and_deps_in_view(view_dir)

    with ev.read("test"):
        remove("mpileaks")
        concretize()

    check_viewdir_removal(view_dir)


def test_env_updates_view_force_remove(
    tmp_path: pathlib.Path, mock_stage, mock_fetch, install_mockery
):
    view_dir = tmp_path / "view"
    env("create", "--with-view=%s" % view_dir, "test")
    with ev.read("test"):
        install("--add", "--fake", "mpileaks")

    check_mpileaks_and_deps_in_view(view_dir)

    with ev.read("test"):
        remove("-f", "mpileaks")

    check_viewdir_removal(view_dir)


def test_env_activate_view_fails(mock_stage, mock_fetch, install_mockery):
    """Sanity check on env activate to make sure it requires shell support"""
    out = env("activate", "test")
    assert "To set up shell support" in out


def test_stack_yaml_definitions(tmp_path: pathlib.Path):
    filename = str(tmp_path / "spack.yaml")
    with open(filename, "w", encoding="utf-8") as f:
        f.write(
            """\
spack:
  definitions:
    - packages: [mpileaks, callpath]
  specs:
    - $packages
"""
        )
    with fs.working_dir(str(tmp_path)):
        env("create", "test", "./spack.yaml")
        test = ev.read("test")

        assert Spec("mpileaks") in test.user_specs
        assert Spec("callpath") in test.user_specs


def test_stack_yaml_definitions_as_constraints(tmp_path: pathlib.Path):
    filename = str(tmp_path / "spack.yaml")
    with open(filename, "w", encoding="utf-8") as f:
        f.write(
            """\
spack:
  definitions:
    - packages: [mpileaks, callpath]
    - mpis: [mpich, openmpi]
  specs:
    - matrix:
      - [$packages]
      - [$^mpis]
"""
        )
    with fs.working_dir(str(tmp_path)):
        env("create", "test", "./spack.yaml")
        test = ev.read("test")

        assert Spec("mpileaks^mpich") in test.user_specs
        assert Spec("callpath^mpich") in test.user_specs
        assert Spec("mpileaks^openmpi") in test.user_specs
        assert Spec("callpath^openmpi") in test.user_specs


def test_stack_yaml_definitions_as_constraints_on_matrix(tmp_path: pathlib.Path):
    filename = str(tmp_path / "spack.yaml")
    with open(filename, "w", encoding="utf-8") as f:
        f.write(
            """\
spack:
  definitions:
    - packages: [mpileaks, callpath]
    - mpis:
      - matrix:
        - [mpich]
        - ['@3.0.4', '@3.0.3']
  specs:
    - matrix:
      - [$packages]
      - [$^mpis]
"""
        )
    with fs.working_dir(str(tmp_path)):
        env("create", "test", "./spack.yaml")
        test = ev.read("test")

        assert Spec("mpileaks^mpich@3.0.4") in test.user_specs
        assert Spec("callpath^mpich@3.0.4") in test.user_specs
        assert Spec("mpileaks^mpich@3.0.3") in test.user_specs
        assert Spec("callpath^mpich@3.0.3") in test.user_specs


@pytest.mark.regression("12095")
def test_stack_yaml_definitions_write_reference(tmp_path: pathlib.Path):
    filename = str(tmp_path / "spack.yaml")
    with open(filename, "w", encoding="utf-8") as f:
        f.write(
            """\
spack:
  definitions:
    - packages: [mpileaks, callpath]
    - indirect: [$packages]
  specs:
    - $packages
"""
        )
    with fs.working_dir(str(tmp_path)):
        env("create", "test", "./spack.yaml")

        with ev.read("test"):
            concretize()
        test = ev.read("test")

        assert Spec("mpileaks") in test.user_specs
        assert Spec("callpath") in test.user_specs


def test_stack_yaml_add_to_list(tmp_path: pathlib.Path):
    filename = str(tmp_path / "spack.yaml")
    with open(filename, "w", encoding="utf-8") as f:
        f.write(
            """\
spack:
  definitions:
    - packages: [mpileaks, callpath]
  specs:
    - $packages
"""
        )
    with fs.working_dir(str(tmp_path)):
        env("create", "test", "./spack.yaml")
        with ev.read("test"):
            add("-l", "packages", "libelf")

        test = ev.read("test")

        assert Spec("libelf") in test.user_specs
        assert Spec("mpileaks") in test.user_specs
        assert Spec("callpath") in test.user_specs


def test_stack_yaml_remove_from_list(tmp_path: pathlib.Path):
    filename = str(tmp_path / "spack.yaml")
    with open(filename, "w", encoding="utf-8") as f:
        f.write(
            """\
spack:
  definitions:
    - packages: [mpileaks, callpath]
  specs:
    - $packages
"""
        )
    with fs.working_dir(str(tmp_path)):
        env("create", "test", "./spack.yaml")
        with ev.read("test"):
            remove("-l", "packages", "mpileaks")

        test = ev.read("test")

        assert Spec("mpileaks") not in test.user_specs
        assert Spec("callpath") in test.user_specs


def test_stack_yaml_remove_from_list_force(tmp_path: pathlib.Path):
    spack_yaml = tmp_path / ev.manifest_name
    spack_yaml.write_text(
        """\
spack:
  definitions:
    - packages: [mpileaks, callpath]
  specs:
    - matrix:
        - [$packages]
        - [^mpich, ^zmpi]
"""
    )

    env("create", "test", str(spack_yaml))
    with ev.read("test"):
        concretize()
        remove("-f", "-l", "packages", "mpileaks")
        find_output = find("-c")

    assert "mpileaks" not in find_output

    test = ev.read("test")
    assert len(test.user_specs) == 2
    assert Spec("callpath ^zmpi") in test.user_specs
    assert Spec("callpath ^mpich") in test.user_specs


def test_stack_yaml_remove_from_matrix_no_effect(tmp_path: pathlib.Path):
    filename = str(tmp_path / "spack.yaml")
    with open(filename, "w", encoding="utf-8") as f:
        f.write(
            """\
spack:
  definitions:
    - packages:
        - matrix:
            - [mpileaks, callpath]
            - [target=default_target]
  specs:
    - $packages
"""
        )
    with fs.working_dir(str(tmp_path)):
        env("create", "test", "./spack.yaml")
        with ev.read("test") as e:
            before = e.user_specs.specs
            remove("-l", "packages", "mpileaks")
            after = e.user_specs.specs

            assert before == after


def test_stack_yaml_force_remove_from_matrix(tmp_path: pathlib.Path):
    filename = str(tmp_path / "spack.yaml")
    with open(filename, "w", encoding="utf-8") as f:
        f.write(
            """\
spack:
  definitions:
    - packages:
        - matrix:
            - [mpileaks, callpath]
            - [target=default_target]
  specs:
    - $packages
"""
        )
    with fs.working_dir(str(tmp_path)):
        env("create", "test", "./spack.yaml")
        with ev.read("test") as e:
            e.concretize()

            before_user = e.user_specs.specs
            concretized_roots_before = e.concretized_roots

            remove("-f", "-l", "packages", "mpileaks")

            after_user = e.user_specs.specs
            concretized_roots_after = e.concretized_roots

            assert before_user == after_user

            mpileaks_spec = Spec("mpileaks target=default_target")
            assert mpileaks_spec in {x.root for x in concretized_roots_before}
            assert mpileaks_spec not in {x.root for x in concretized_roots_after}


def test_stack_definition_extension(tmp_path: pathlib.Path):
    filename = str(tmp_path / "spack.yaml")
    with open(filename, "w", encoding="utf-8") as f:
        f.write(
            """\
spack:
  definitions:
    - packages: [libelf, mpileaks]
    - packages: [callpath]
  specs:
    - $packages
"""
        )
    with fs.working_dir(str(tmp_path)):
        env("create", "test", "./spack.yaml")

        test = ev.read("test")

        assert Spec("libelf") in test.user_specs
        assert Spec("mpileaks") in test.user_specs
        assert Spec("callpath") in test.user_specs


def test_stack_definition_conditional_false(tmp_path: pathlib.Path):
    filename = str(tmp_path / "spack.yaml")
    with open(filename, "w", encoding="utf-8") as f:
        f.write(
            """\
spack:
  definitions:
    - packages: [libelf, mpileaks]
    - packages: [callpath]
      when: 'False'
  specs:
    - $packages
"""
        )
    with fs.working_dir(str(tmp_path)):
        env("create", "test", "./spack.yaml")

        test = ev.read("test")

        assert Spec("libelf") in test.user_specs
        assert Spec("mpileaks") in test.user_specs
        assert Spec("callpath") not in test.user_specs


def test_stack_definition_conditional_true(tmp_path: pathlib.Path):
    filename = str(tmp_path / "spack.yaml")
    with open(filename, "w", encoding="utf-8") as f:
        f.write(
            """\
spack:
  definitions:
    - packages: [libelf, mpileaks]
    - packages: [callpath]
      when: 'True'
  specs:
    - $packages
"""
        )
    with fs.working_dir(str(tmp_path)):
        env("create", "test", "./spack.yaml")

        test = ev.read("test")

        assert Spec("libelf") in test.user_specs
        assert Spec("mpileaks") in test.user_specs
        assert Spec("callpath") in test.user_specs


def test_stack_definition_conditional_with_variable(tmp_path: pathlib.Path):
    filename = str(tmp_path / "spack.yaml")
    with open(filename, "w", encoding="utf-8") as f:
        f.write(
            """\
spack:
  definitions:
    - packages: [libelf, mpileaks]
    - packages: [callpath]
      when: platform == 'test'
  specs:
    - $packages
"""
        )
    with fs.working_dir(str(tmp_path)):
        env("create", "test", "./spack.yaml")

        test = ev.read("test")

        assert Spec("libelf") in test.user_specs
        assert Spec("mpileaks") in test.user_specs
        assert Spec("callpath") in test.user_specs


def test_stack_definition_conditional_with_satisfaction(tmp_path: pathlib.Path):
    filename = str(tmp_path / "spack.yaml")
    with open(filename, "w", encoding="utf-8") as f:
        f.write(
            """\
spack:
  definitions:
    - packages: [libelf, mpileaks]
      when: arch.satisfies('platform=foo')  # will be "test" when testing
    - packages: [callpath]
      when: arch.satisfies('platform=test')
  specs:
    - $packages
"""
        )
    with fs.working_dir(str(tmp_path)):
        env("create", "test", "./spack.yaml")

        test = ev.read("test")

        assert Spec("libelf") not in test.user_specs
        assert Spec("mpileaks") not in test.user_specs
        assert Spec("callpath") in test.user_specs


def test_stack_definition_complex_conditional(tmp_path: pathlib.Path):
    filename = str(tmp_path / "spack.yaml")
    with open(filename, "w", encoding="utf-8") as f:
        f.write(
            """\
spack:
  definitions:
    - packages: [libelf, mpileaks]
    - packages: [callpath]
      when: re.search(r'foo', hostname) and env['test'] == 'THISSHOULDBEFALSE'
  specs:
    - $packages
"""
        )
    with fs.working_dir(str(tmp_path)):
        env("create", "test", "./spack.yaml")

        test = ev.read("test")

        assert Spec("libelf") in test.user_specs
        assert Spec("mpileaks") in test.user_specs
        assert Spec("callpath") not in test.user_specs


def test_stack_definition_conditional_invalid_variable(tmp_path: pathlib.Path):
    filename = str(tmp_path / "spack.yaml")
    with open(filename, "w", encoding="utf-8") as f:
        f.write(
            """\
spack:
  definitions:
    - packages: [libelf, mpileaks]
    - packages: [callpath]
      when: bad_variable == 'test'
  specs:
    - $packages
"""
        )
    with fs.working_dir(str(tmp_path)):
        with pytest.raises(NameError):
            env("create", "test", "./spack.yaml")


def test_stack_definition_conditional_add_write(tmp_path: pathlib.Path):
    filename = str(tmp_path / "spack.yaml")
    with open(filename, "w", encoding="utf-8") as f:
        f.write(
            """\
spack:
  definitions:
    - packages: [libelf, mpileaks]
    - packages: [callpath]
      when: platform == 'test'
  specs:
    - $packages
"""
        )
    with fs.working_dir(str(tmp_path)):
        env("create", "test", "./spack.yaml")
        with ev.read("test"):
            add("-l", "packages", "zmpi")

        test = ev.read("test")

        packages_lists = list(
            filter(lambda x: "packages" in x, test.manifest["spack"]["definitions"])
        )

        assert len(packages_lists) == 2
        assert "callpath" not in packages_lists[0]["packages"]
        assert "callpath" in packages_lists[1]["packages"]
        assert "zmpi" in packages_lists[0]["packages"]
        assert "zmpi" not in packages_lists[1]["packages"]


def test_stack_combinatorial_view(
    installed_environment, template_combinatorial_env, tmp_path: pathlib.Path
):
    """Tests creating a default view for a combinatorial stack."""
    view_dir = tmp_path / "view"
    with installed_environment(template_combinatorial_env.format(view_config="")) as test:
        for spec in traverse_nodes(test.concrete_roots(), deptype=("link", "run")):
            if spec.name == "gcc-runtime":
                continue
            current_dir = view_dir / f"{spec.architecture.target}" / f"{spec.name}-{spec.version}"
            assert current_dir.exists() and current_dir.is_dir()


def test_stack_view_select(
    installed_environment, template_combinatorial_env, tmp_path: pathlib.Path
):
    view_dir = tmp_path / "view"
    content = template_combinatorial_env.format(view_config="select: ['target=x86_64']\n")
    with installed_environment(content) as test:
        for spec in traverse_nodes(test.concrete_roots(), deptype=("link", "run")):
            if spec.name == "gcc-runtime":
                continue
            current_dir = view_dir / f"{spec.architecture.target}" / f"{spec.name}-{spec.version}"
            assert current_dir.exists() is spec.satisfies("target=x86_64")


def test_stack_view_exclude(
    installed_environment, template_combinatorial_env, tmp_path: pathlib.Path
):
    view_dir = tmp_path / "view"
    content = template_combinatorial_env.format(view_config="exclude: [callpath]\n")
    with installed_environment(content) as test:
        for spec in traverse_nodes(test.concrete_roots(), deptype=("link", "run")):
            if spec.name == "gcc-runtime":
                continue
            current_dir = view_dir / f"{spec.architecture.target}" / f"{spec.name}-{spec.version}"
            assert current_dir.exists() is not spec.satisfies("callpath")


def test_stack_view_select_and_exclude(
    installed_environment, template_combinatorial_env, tmp_path: pathlib.Path
):
    view_dir = tmp_path / "view"
    content = template_combinatorial_env.format(
        view_config="""select: ['target=x86_64']
          exclude: [callpath]
"""
    )
    with installed_environment(content) as test:
        for spec in traverse_nodes(test.concrete_roots(), deptype=("link", "run")):
            if spec.name == "gcc-runtime":
                continue
            current_dir = view_dir / f"{spec.architecture.target}" / f"{spec.name}-{spec.version}"
            assert current_dir.exists() is (
                spec.satisfies("target=x86_64") and not spec.satisfies("callpath")
            )


def test_view_link_roots(
    installed_environment, template_combinatorial_env, tmp_path: pathlib.Path
):
    view_dir = tmp_path / "view"
    content = template_combinatorial_env.format(
        view_config="""select: ['target=x86_64']
          exclude: [callpath]
          link: 'roots'
    """
    )
    with installed_environment(content) as test:
        for spec in traverse_nodes(test.concrete_roots(), deptype=("link", "run")):
            if spec.name == "gcc-runtime":
                continue
            current_dir = view_dir / f"{spec.architecture.target}" / f"{spec.name}-{spec.version}"
            expected_exists = spec in test.roots() and (
                spec.satisfies("target=x86_64") and not spec.satisfies("callpath")
            )
            assert current_dir.exists() == expected_exists


def test_view_link_run(
    tmp_path: pathlib.Path, mock_fetch, mock_packages, mock_archive, install_mockery
):
    yaml = str(tmp_path / "spack.yaml")
    viewdir = str(tmp_path / "view")
    envdir = str(tmp_path)
    with open(yaml, "w", encoding="utf-8") as f:
        f.write(
            """
spack:
  specs:
  - dttop

  view:
    combinatorial:
      root: %s
      link: run
      projections:
        all: '{name}'"""
            % viewdir
        )

    with ev.Environment(envdir):
        install("--fake")

    # make sure transitive run type deps are in the view
    for pkg in ("dtrun1", "dtrun3"):
        assert os.path.exists(os.path.join(viewdir, pkg))

    # and non-run-type deps are not.
    for pkg in (
        "dtlink1",
        "dtlink2",
        "dtlink3",
        "dtlink4",
        "dtlink5dtbuild1",
        "dtbuild2",
        "dtbuild3",
    ):
        assert not os.path.exists(os.path.join(viewdir, pkg))


@pytest.mark.parametrize("link_type", ["hardlink", "copy", "symlink"])
def test_view_link_type(link_type, installed_environment, tmp_path: pathlib.Path):
    view_dir = tmp_path / "view"
    with installed_environment(
        f"""\
spack:
  specs:
    - mpileaks
  view:
    default:
      root: {view_dir}
      link_type: {link_type}"""
    ) as test:
        for spec in test.roots():
            # Assertions are based on the behavior of the "--fake" install
            bin_file = pathlib.Path(test.default_view.view()._root) / "bin" / spec.name
            assert bin_file.exists()
            assert bin_file.is_symlink() == (link_type == "symlink")


def test_view_link_all(installed_environment, template_combinatorial_env, tmp_path: pathlib.Path):
    view_dir = tmp_path / "view"
    content = template_combinatorial_env.format(
        view_config="""select: ['target=x86_64']
          exclude: [callpath]
          link: 'all'
    """
    )

    with installed_environment(content) as test:
        for spec in traverse_nodes(test.concrete_roots(), deptype=("link", "run")):
            if spec.name == "gcc-runtime":
                continue
            current_dir = view_dir / f"{spec.architecture.target}" / f"{spec.name}-{spec.version}"
            assert current_dir.exists() == (
                spec.satisfies("target=x86_64") and not spec.satisfies("callpath")
            )


def test_stack_view_activate_from_default(
    installed_environment, template_combinatorial_env, tmp_path: pathlib.Path
):
    view_dir = tmp_path / "view"
    content = template_combinatorial_env.format(view_config="select: ['target=x86_64']")
    # Replace the name of the view
    content = content.replace("combinatorial:", "default:")
    with installed_environment(content):
        shell = env("activate", "--sh", "test")
        assert "PATH" in shell, shell
        assert str(view_dir / "bin") in shell
        assert "FOOBAR=mpileaks" in shell


def test_envvar_set_in_activate(tmp_path: pathlib.Path, mock_packages, install_mockery):
    spack_yaml = tmp_path / "spack.yaml"
    env_vars_yaml = tmp_path / "env_vars.yaml"

    env_vars_yaml.write_text(
        """
env_vars:
  set:
    CONFIG_ENVAR_SET_IN_ENV_LOAD: "True"
"""
    )

    spack_yaml.write_text(
        """
spack:
  include:
  - env_vars.yaml
  specs:
    - cmake%gcc
  env_vars:
    set:
      SPACK_ENVAR_SET_IN_ENV_LOAD: "True"
"""
    )

    env("create", "test", str(spack_yaml))
    with ev.read("test"):
        install("--fake")

    test_env = ev.read("test")
    output = env("activate", "--sh", "test")

    assert "SPACK_ENVAR_SET_IN_ENV_LOAD=True" in output
    assert "CONFIG_ENVAR_SET_IN_ENV_LOAD=True" in output

    with test_env:
        with spack.util.environment.set_env(
            SPACK_ENVAR_SET_IN_ENV_LOAD="True", CONFIG_ENVAR_SET_IN_ENV_LOAD="True"
        ):
            output = env("deactivate", "--sh")
            assert "unset SPACK_ENVAR_SET_IN_ENV_LOAD" in output
            assert "unset CONFIG_ENVAR_SET_IN_ENV_LOAD" in output


def test_stack_view_no_activate_without_default(
    installed_environment, template_combinatorial_env, tmp_path: pathlib.Path
):
    view_dir = tmp_path / "view"
    content = template_combinatorial_env.format(view_config="select: ['target=x86_64']")
    with installed_environment(content):
        shell = env("activate", "--sh", "test")
        assert "PATH" not in shell
        assert str(view_dir) not in shell


@pytest.mark.parametrize("include_views", [True, False, "split"])
def test_stack_view_multiple_views(installed_environment, tmp_path: pathlib.Path, include_views):
    """Test multiple views as both included views (True), as both environment
    views (False), or as one included and the other in the environment.
    """
    # Write the view configuration and or manifest file
    view_filename = tmp_path / "view.yaml"
    base_content = """\
  definitions:
    - packages: [mpileaks, cmake]
    - targets: ['target=x86_64', 'target=core2']
  specs:
    - matrix:
        - [$packages]
        - [$targets]
"""

    include_content = f"  include:\n    - {view_filename}\n"
    view_line = "  view:\n"

    comb_dir = tmp_path / "combinatorial-view"
    comb_view = """\
{0}combinatorial:
{0}  root: {1}
{0}  exclude: [target=core2]
{0}  projections:
"""

    projection = "    'all': '{architecture.target}/{name}-{version}'"

    default_dir = tmp_path / "default-view"
    default_view = """\
{0}default:
{0}  root: {1}
{0}  select: ['target=x86_64']
"""

    content = "spack:\n"
    indent = "  "
    if include_views is True:
        # Include both the gcc and combinatorial views
        view = "view:\n" + default_view.format(indent, str(default_dir))
        view += comb_view.format(indent, str(comb_dir)) + indent + projection
        view_filename.write_text(view)
        content += include_content + base_content
    elif include_views == "split":
        # Include the gcc view and inline the combinatorial view
        view = "view:\n" + default_view.format(indent, str(default_dir))
        view_filename.write_text(view)
        content += include_content + base_content + view_line
        indent += "  "
        content += comb_view.format(indent, str(comb_dir)) + indent + projection
    else:
        # Inline both the gcc and combinatorial views in the environment.
        indent += "  "
        content += base_content + view_line
        content += default_view.format(indent, str(default_dir))
        content += comb_view.format(indent, str(comb_dir)) + indent + projection

    with installed_environment(content) as e:
        assert os.path.exists(str(default_dir / "bin"))
        for spec in traverse_nodes(e.concrete_roots(), deptype=("link", "run")):
            if spec.name == "gcc-runtime":
                continue
            current_dir = comb_dir / f"{spec.architecture.target}" / f"{spec.name}-{spec.version}"
            assert current_dir.exists() is not spec.satisfies("target=core2")


def test_env_activate_sh_prints_shell_output(mock_stage, mock_fetch, install_mockery):
    """Check the shell commands output by ``spack env activate --sh``.

    This is a cursory check; ``share/spack/qa/setup-env-test.sh`` checks
    for correctness.
    """
    env("create", "test")

    out = env("activate", "--sh", "test")
    assert "export SPACK_ENV=" in out
    assert "export PS1=" not in out
    assert "alias despacktivate=" in out

    out = env("activate", "--sh", "--prompt", "test")
    assert "export SPACK_ENV=" in out
    assert "export PS1=" in out
    assert "alias despacktivate=" in out


def test_env_activate_csh_prints_shell_output(mock_stage, mock_fetch, install_mockery):
    """Check the shell commands output by ``spack env activate --csh``."""
    env("create", "test")

    out = env("activate", "--csh", "test")
    assert "setenv SPACK_ENV" in out
    assert "setenv set prompt" not in out
    assert "alias despacktivate" in out

    out = env("activate", "--csh", "--prompt", "test")
    assert "setenv SPACK_ENV" in out
    assert "set prompt=" in out
    assert "alias despacktivate" in out


@pytest.mark.regression("12719")
def test_env_activate_default_view_root_unconditional(mutable_mock_env_path):
    """Check that the root of the default view in the environment is added
    to the shell unconditionally."""
    env("create", "test")

    with ev.read("test") as e:
        viewdir = e.default_view.root

    out = env("activate", "--sh", "test")
    viewdir_bin = os.path.join(viewdir, "bin")

    assert (
        "export PATH={0}".format(viewdir_bin) in out
        or "export PATH='{0}".format(viewdir_bin) in out
        or 'export PATH="{0}'.format(viewdir_bin) in out
    )


def test_env_activate_custom_view(tmp_path: pathlib.Path, mock_packages):
    """Check that an environment can be activated with a non-default view."""
    env_template = tmp_path / "spack.yaml"
    default_dir = tmp_path / "defaultdir"
    nondefaultdir = tmp_path / "nondefaultdir"
    with open(env_template, "w", encoding="utf-8") as f:
        f.write(
            f"""\
spack:
  specs: [a]
  view:
    default:
      root: {default_dir}
    nondefault:
      root: {nondefaultdir}"""
        )
    env("create", "test", str(env_template))
    shell = env("activate", "--sh", "--with-view", "nondefault", "test")
    assert os.path.join(nondefaultdir, "bin") in shell


def test_concretize_user_specs_together(mutable_config):
    with ev.create("coconcretization") as e:
        mutable_config.set("concretizer:unify", True)

        # Concretize a first time using 'mpich' as the MPI provider
        e.add("mpileaks")
        e.add("mpich")
        e.concretize()

        assert all("mpich" in spec for _, spec in e.concretized_specs())
        assert all("mpich2" not in spec for _, spec in e.concretized_specs())

        # Concretize a second time using 'mpich2' as the MPI provider
        e.remove("mpich")
        e.add("mpich2")

        exc_cls = spack.error.UnsatisfiableSpecError

        # Concretizing without invalidating the concrete spec for mpileaks fails
        with pytest.raises(exc_cls):
            e.concretize()
        e.concretize(force=True)

        assert all("mpich2" in spec for _, spec in e.concretized_specs())
        assert all("mpich" not in spec for _, spec in e.concretized_specs())

        # Concretize again without changing anything, check everything
        # stays the same
        e.concretize()

        assert all("mpich2" in spec for _, spec in e.concretized_specs())
        assert all("mpich" not in spec for _, spec in e.concretized_specs())


def test_duplicate_packages_raise_when_concretizing_together(mutable_config):
    with ev.create("coconcretization") as e:
        mutable_config.set("concretizer:unify", True)
        e.add("mpileaks+opt")
        e.add("mpileaks~opt")
        e.add("mpich")

        exc_cls = spack.error.UnsatisfiableSpecError
        match = r"You could consider setting `concretizer:unify`"

        with pytest.raises(exc_cls, match=match):
            e.concretize()


def test_env_write_only_non_default():
    env("create", "test")

    e = ev.read("test")
    with open(e.manifest_path, "r", encoding="utf-8") as f:
        yaml = f.read()

    assert yaml == ev.default_manifest_yaml()


@pytest.mark.regression("20526")
def test_env_write_only_non_default_nested(tmp_path: pathlib.Path):
    # setup an environment file
    # the environment includes configuration because nested configs proved the
    # most difficult to avoid writing.
    filename = "spack.yaml"
    filepath = str(tmp_path / filename)
    contents = """\
spack:
  specs:
  - matrix:
    - [mpileaks]
  packages:
    all:
      compiler: [gcc]
  view: true
"""

    # create environment with some structure
    with open(filepath, "w", encoding="utf-8") as f:
        f.write(contents)
    env("create", "test", filepath)

    # concretize
    with ev.read("test") as e:
        concretize()
        e.write()

        with open(e.manifest_path, "r", encoding="utf-8") as f:
            manifest = f.read()

    assert manifest == contents


@pytest.mark.regression("18147")
def test_can_update_attributes_with_override(tmp_path: pathlib.Path):
    spack_yaml = """
spack:
  mirrors::
    test: /foo/bar
  packages:
    cmake:
      paths:
        cmake@3.18.1: /usr
  specs:
  - hdf5
"""
    abspath = tmp_path / "spack.yaml"
    abspath.write_text(spack_yaml)

    # Check that an update does not raise
    env("update", "-y", str(tmp_path))


@pytest.mark.regression("18338")
def test_newline_in_commented_sequence_is_not_an_issue(tmp_path: pathlib.Path):
    spack_yaml = """
spack:
  specs:
  - dyninst
  packages:
    libelf:
      externals:
      - spec: libelf@0.8.13
        modules:
        - libelf/3.18.1

  concretizer:
    unify: false
"""
    abspath = tmp_path / "spack.yaml"
    abspath.write_text(spack_yaml)

    def extract_dag_hash(environment):
        _, dyninst = next(iter(environment.specs_by_hash.items()))
        return dyninst["libelf"].dag_hash()

    # Concretize a first time and create a lockfile
    with ev.Environment(str(tmp_path)) as e:
        concretize()
        libelf_first_hash = extract_dag_hash(e)

    # Check that a second run won't error
    with ev.Environment(str(tmp_path)) as e:
        concretize()
        libelf_second_hash = extract_dag_hash(e)

    assert libelf_first_hash == libelf_second_hash


@pytest.mark.regression("18441")
def test_lockfile_not_deleted_on_write_error(tmp_path: pathlib.Path, monkeypatch):
    raw_yaml = """
spack:
  specs:
  - dyninst
  packages:
    libelf:
      externals:
      - spec: libelf@0.8.13
        prefix: /usr
"""
    spack_yaml = tmp_path / "spack.yaml"
    spack_yaml.write_text(raw_yaml)
    spack_lock = tmp_path / "spack.lock"

    # Concretize a first time and create a lockfile
    with ev.Environment(str(tmp_path)):
        concretize()
    assert os.path.exists(str(spack_lock))

    # If I run concretize again and there's an error during write,
    # the spack.lock file shouldn't disappear from disk
    def _write_helper_raise(self):
        raise RuntimeError("some error")

    monkeypatch.setattr(ev.environment.EnvironmentManifestFile, "flush", _write_helper_raise)
    with ev.Environment(str(tmp_path)) as e:
        e.concretize(force=True)
        with pytest.raises(RuntimeError):
            e.clear()
            e.write()
    assert os.path.exists(str(spack_lock))


def _setup_develop_packages(tmp_path: pathlib.Path):
    """Sets up a structure ./init_env/spack.yaml, ./build_folder, ./dest_env
    where spack.yaml has a relative develop path to build_folder"""
    init_env = tmp_path / "init_env"
    build_folder = tmp_path / "build_folder"
    dest_env = tmp_path / "dest_env"

    init_env.mkdir(parents=True, exist_ok=True)
    build_folder.mkdir(parents=True, exist_ok=True)
    dest_env.mkdir(parents=True, exist_ok=True)

    raw_yaml = """
spack:
  specs: ['mypkg1', 'mypkg2']
  develop:
    mypkg1:
      path: ../build_folder
      spec: mypkg@main
    mypkg2:
      path: /some/other/path
      spec: mypkg@main
"""
    spack_yaml = init_env / "spack.yaml"
    spack_yaml.write_text(raw_yaml)

    return init_env, build_folder, dest_env, spack_yaml


def test_rewrite_rel_dev_path_new_dir(tmp_path: pathlib.Path):
    """Relative develop paths should be rewritten for new environments in
    a different directory from the original manifest file"""
    _, build_folder, dest_env, spack_yaml = _setup_develop_packages(tmp_path)

    env("create", "-d", str(dest_env), str(spack_yaml))
    with ev.Environment(str(dest_env)) as e:
        assert e.dev_specs["mypkg1"]["path"] == str(build_folder)
        assert e.dev_specs["mypkg2"]["path"] == sep + os.path.join("some", "other", "path")


def test_rewrite_rel_dev_path_named_env(tmp_path: pathlib.Path):
    """Relative develop paths should by default be rewritten for new named
    environment"""
    _, build_folder, _, spack_yaml = _setup_develop_packages(tmp_path)
    env("create", "named_env", str(spack_yaml))
    with ev.read("named_env") as e:
        assert e.dev_specs["mypkg1"]["path"] == str(build_folder)
        assert e.dev_specs["mypkg2"]["path"] == sep + os.path.join("some", "other", "path")


def test_does_not_rewrite_rel_dev_path_when_keep_relative_is_set(tmp_path: pathlib.Path):
    """Relative develop paths should not be rewritten when --keep-relative is
    passed to create"""
    _, _, _, spack_yaml = _setup_develop_packages(tmp_path)
    env("create", "--keep-relative", "named_env", str(spack_yaml))
    with ev.read("named_env") as e:
        assert e.dev_specs["mypkg1"]["path"] == "../build_folder"
        assert e.dev_specs["mypkg2"]["path"] == "/some/other/path"


@pytest.mark.regression("23440")
def test_custom_version_concretize_together(mutable_config):
    # Custom versions should be permitted in specs when
    # concretizing together
    with ev.create("custom_version") as e:
        mutable_config.set("concretizer:unify", True)
        # Concretize a first time using 'mpich' as the MPI provider
        e.add("hdf5@=myversion")
        e.add("mpich")
        e.concretize()
        assert any(spec.satisfies("hdf5@myversion") for _, spec in e.concretized_specs())


def test_modules_relative_to_views(environment_from_manifest, install_mockery, mock_fetch):
    environment_from_manifest(
        """
spack:
  specs:
  - trivial-install-test-package
  modules:
    default:
      enable:: [tcl]
      use_view: true
      roots:
        tcl: modules
"""
    )

    with ev.read("test") as e:
        install("--fake")
        user_spec_hash = e.concretized_roots[0].hash
        spec = e.specs_by_hash[user_spec_hash]
        view_prefix = e.default_view.get_projection_for_spec(spec)
        modules_glob = "%s/modules/**/*/*" % e.path
        modules = glob.glob(modules_glob)
        assert len(modules) == 1
        module = modules[0]

    with open(module, "r", encoding="utf-8") as f:
        contents = f.read()

    assert view_prefix in contents
    assert spec.prefix not in contents


def test_modules_exist_after_env_install(installed_environment, monkeypatch):
    # Some caching issue
    monkeypatch.setattr(spack.modules.tcl.TclConfiguration, "_registry", {})
    with installed_environment(
        """
spack:
  specs:
  - mpileaks
  modules:
    default:
      enable:: [tcl]
      use_view: true
      roots:
        tcl: uses_view
    full:
      enable:: [tcl]
      roots:
        tcl: without_view
"""
    ) as e:
        specs = e.all_specs()
        for module_set in ("uses_view", "without_view"):
            modules = glob.glob(f"{e.path}/{module_set}/**/*/*")
            assert len(modules) == len(specs), "Not all modules were generated"
            for spec in specs:
                if spec.external:
                    continue

                module = next((m for m in modules if os.path.dirname(m).endswith(spec.name)), None)
                assert module, f"Module for {spec} not found"

                # Now verify that modules have paths pointing into the view instead of the package
                # prefix if and only if they set use_view to true.
                with open(module, "r", encoding="utf-8") as f:
                    contents = f.read()

                if module_set == "uses_view":
                    assert e.default_view.get_projection_for_spec(spec) in contents
                    assert spec.prefix not in contents
                else:
                    assert e.default_view.get_projection_for_spec(spec) not in contents
                    assert spec.prefix in contents


@pytest.mark.disable_clean_stage_check
def test_install_develop_keep_stage(
    environment_from_manifest, install_mockery, mock_fetch, monkeypatch, tmp_path: pathlib.Path
):
    """Develop a dependency of a package and make sure that the associated
    stage for the package is retained after a successful install.
    """
    environment_from_manifest(
        """
spack:
  specs:
  - mpileaks
"""
    )

    monkeypatch.setattr(spack.stage.DevelopStage, "destroy", _always_fail)

    with ev.read("test") as e:
        libelf_dev_path = tmp_path / "libelf-test-dev-path"
        libelf_dev_path.mkdir(parents=True)
        develop(f"--path={libelf_dev_path}", "libelf@0.8.13")
        concretize()
        (libelf_spec,) = e.all_matching_specs("libelf")
        (mpileaks_spec,) = e.all_matching_specs("mpileaks")
        assert not os.path.exists(libelf_spec.package.stage.path)
        assert not os.path.exists(mpileaks_spec.package.stage.path)
        install("--fake")
        assert os.path.exists(libelf_spec.package.stage.path)
        assert not os.path.exists(mpileaks_spec.package.stage.path)


# Helper method for test_install_develop_keep_stage
def _always_fail(cls, *args, **kwargs):
    raise Exception("Restage or destruction of dev stage detected during install")


@pytest.mark.regression("24148")
def test_virtual_spec_concretize_together(mutable_config):
    # An environment should permit to concretize "mpi"
    with ev.create("virtual_spec") as e:
        mutable_config.set("concretizer:unify", True)
        e.add("mpi")
        e.concretize()
        assert any(s.package.provides("mpi") for _, s in e.concretized_specs())


@pytest.mark.parametrize(
    "unify,method_to_fail",
    [
        (True, (spack.concretize, "concretize_together")),
        ("when_possible", (spack.solver.asp.Solver, "solve_in_rounds")),
        # An earlier failure so that we test the case where the internal state
        # has been changed, but the pointer to the internal variables has not change.
        # This effectively tests that we are properly copying by value not by
        # reference for the transactional concretization
        (True, (ev.EnvironmentConcretizer, "concretize")),
    ],
)
def test_concretize_transactional(unify, method_to_fail, monkeypatch, mutable_config):
    spack.config.set("concretizer:unify", unify)
    e = ev.create("test")

    e.add("mpi")
    e.add("zlib")
    e.concretize()

    # remove one spec and add another to ensure we test with changes before
    # and after the environment is cleared during concretization
    e.remove("zlib")
    e.add("libelf")

    def fail(*args, **kwargs):
        raise Exception("Test failures")

    location, method = method_to_fail
    monkeypatch.setattr(location, method, fail)

    first_roots = e.concretized_roots[:]
    first_hash_dict = e.specs_by_hash.copy()

    try:
        e.concretize()
    except Exception:
        pass
    else:
        assert False, "concretization failure required for testing"

    assert e.concretized_roots == first_roots
    assert e.specs_by_hash == first_hash_dict


def test_query_develop_specs(tmp_path: pathlib.Path):
    """Test whether a spec is develop'ed or not"""
    srcdir = tmp_path / "here"
    srcdir.mkdir()

    env("create", "test")
    with ev.read("test") as e:
        e.add("mpich")
        e.add("mpileaks")
        develop("--no-clone", "-p", str(srcdir), "mpich@=1")

        assert e.is_develop(Spec("mpich"))
        assert not e.is_develop(Spec("mpileaks"))


@pytest.mark.parametrize("method", [spack.cmd.env.env_activate, spack.cmd.env.env_deactivate])
@pytest.mark.parametrize(
    "env,no_env,env_dir", [("b", False, None), (None, True, None), (None, False, "path/")]
)
def test_activation_and_deactivation_ambiguities(method, env, no_env, env_dir, capfd):
    """spack [-e x | -E | -D x/]  env [activate | deactivate] y are ambiguous"""
    args = Namespace(
        shell="sh", env_name="a", env=env, no_env=no_env, env_dir=env_dir, keep_relative=False
    )
    with pytest.raises(SystemExit):
        method(args)
    _, err = capfd.readouterr()
    assert "is ambiguous" in err


@pytest.mark.regression("26548")
def test_custom_store_in_environment(mutable_config, tmp_path: pathlib.Path):
    spack_yaml = tmp_path / "spack.yaml"
    install_root = tmp_path / "store"
    spack_yaml.write_text(
        """
spack:
  specs:
  - libelf
  config:
    install_tree:
      root: {0}
""".format(install_root)
    )
    current_store_root = str(spack.store.STORE.root)
    assert str(current_store_root) != str(install_root)
    with ev.Environment(str(tmp_path)):
        assert str(spack.store.STORE.root) == str(install_root)
    assert str(spack.store.STORE.root) == current_store_root


def test_activate_temp(monkeypatch, tmp_path: pathlib.Path):
    """Tests whether `spack env activate --temp` creates an environment in a
    temporary directory"""
    env_dir = lambda: str(tmp_path)
    monkeypatch.setattr(spack.cmd.env, "create_temp_env_directory", env_dir)
    shell = env("activate", "--temp", "--sh")
    active_env_var = next(line for line in shell.splitlines() if ev.spack_env_var in line)
    assert str(tmp_path) in active_env_var
    assert ev.is_env_dir(str(tmp_path))


@pytest.mark.parametrize(
    "conflict_arg", [["--dir"], ["--keep-relative"], ["--with-view", "foo"], ["env"]]
)
def test_activate_parser_conflicts_with_temp(conflict_arg):
    with pytest.raises(SpackCommandError):
        env("activate", "--sh", "--temp", *conflict_arg)


def test_create_and_activate_managed(tmp_path: pathlib.Path):
    with fs.working_dir(str(tmp_path)):
        shell = env("activate", "--without-view", "--create", "--sh", "foo")
        active_env_var = next(line for line in shell.splitlines() if ev.spack_env_var in line)
        assert str(tmp_path) in active_env_var
        active_ev = ev.active_environment()
        assert active_ev and "foo" == active_ev.name
        env("deactivate")


def test_create_and_activate_independent(tmp_path: pathlib.Path):
    with fs.working_dir(str(tmp_path)):
        env_dir = os.path.join(str(tmp_path), "foo")
        shell = env("activate", "--without-view", "--create", "--sh", env_dir)
        active_env_var = next(line for line in shell.splitlines() if ev.spack_env_var in line)
        assert str(env_dir) in active_env_var
        assert ev.is_env_dir(env_dir)
        env("deactivate")


def test_activate_default(monkeypatch):
    """Tests whether `spack env activate` creates / activates the default
    environment"""
    assert not ev.exists("default")

    # Activating it the first time should create it
    env("activate", "--sh")
    env("deactivate", "--sh")
    assert ev.exists("default")

    # Activating it while it already exists should work
    env("activate", "--sh")
    env("deactivate", "--sh")
    assert ev.exists("default")

    env("remove", "-y", "default")
    assert not ev.exists("default")


def test_env_view_fail_if_symlink_points_elsewhere(
    tmp_path: pathlib.Path, install_mockery, mock_fetch
):
    view = str(tmp_path / "view")
    # Put a symlink to an actual directory in view
    non_view_dir = str(tmp_path / "dont-delete-me")
    os.mkdir(non_view_dir)
    os.symlink(non_view_dir, view)
    with ev.create("env", with_view=view):
        add("libelf")
        install("--fake")
    assert os.path.isdir(non_view_dir)


def test_env_view_backward_compat_old_symlink_format(
    tmp_path: pathlib.Path, mock_stage, mock_fetch, install_mockery
):
    """An old-style symlink view pointing to ._view/<hash>/ is recognized as up-to-date."""
    view = str(tmp_path / "view")
    env("create", "--with-view={0}".format(view), "test")
    with ev.read("test") as e:
        add("libelf")
        install("--fake")
        view_desc = e.default_view
        specs = view_desc.specs_for_view(e.concrete_roots())
        content_hash = view_desc.content_hash(specs)

    # Simulate old format: remove the real view dir and replace with a symlink
    root_name = os.path.basename(view)
    root_dir = os.path.dirname(view)
    old_hash_dir = os.path.join(root_dir, "._%s" % root_name, content_hash)
    shutil.rmtree(view)
    os.makedirs(old_hash_dir, exist_ok=True)
    os.symlink(old_hash_dir, view)

    # Regeneration should detect the old symlink is up-to-date and be a no-op
    with ev.read("test"):
        env("view", "regenerate")

    # The symlink was not touched (still points to the old hash dir)
    assert os.path.islink(view)
    assert os.path.realpath(view) == old_hash_dir


def test_failed_view_cleanup(tmp_path: pathlib.Path, mock_stage, mock_fetch, install_mockery):
    """Tests whether Spack cleans up after itself when a view fails to create"""
    view_dir = tmp_path / "view"
    with ev.create("env", with_view=str(view_dir)):
        add("libelf")
        install("--fake")

    assert view_dir.is_dir() and not view_dir.is_symlink()

    # Add a spec that results in view clash when creating a view
    with ev.read("env"):
        add("libelf cflags=-O3")
        with pytest.raises(ev.SpackEnvironmentViewError):
            install("--fake")

    # The view is still a real directory; no .new or .old leftovers.
    assert view_dir.is_dir() and not view_dir.is_symlink()
    assert not list(tmp_path.glob("view.new.*"))
    assert not list(tmp_path.glob("view.old.*"))


def test_environment_view_target_already_exists(
    tmp_path: pathlib.Path, mock_stage, mock_fetch, install_mockery
):
    """The view is a real directory; an orphaned ._view/<hash>/ from the old
    symlink format does not prevent regeneration."""

    view = str(tmp_path / "view")
    env("create", "--with-view={0}".format(view), "test")
    with ev.read("test"):
        add("libelf")
        install("--fake")

    # view is a real directory in the new format
    assert os.path.isdir(view) and not os.path.islink(view)
    assert os.listdir(view)  # has some contents

    # Simulate an orphaned old-format hash dir sitting next to the view
    orphan_dir = os.path.join(str(tmp_path), "._view", "someoldhash")
    os.makedirs(orphan_dir)
    fs.touch(os.path.join(orphan_dir, "orphan_file"))

    # Regeneration should succeed; the orphaned dir is not touched
    with ev.read("test"):
        env("view", "regenerate")

    assert os.path.isdir(view) and not os.path.islink(view)
    assert os.path.isfile(os.path.join(orphan_dir, "orphan_file"))


def test_environment_query_spec_by_hash(mock_stage, mock_fetch, install_mockery):
    env("create", "test")
    with ev.read("test"):
        add("libdwarf")
        concretize()
    with ev.read("test") as e:
        spec = e.matching_spec("libelf")
        install("--fake", f"/{spec.dag_hash()}")
    with ev.read("test") as e:
        assert not e.matching_spec("libdwarf").installed
        assert e.matching_spec("libelf").installed


@pytest.mark.parametrize("lockfile", ["v1", "v2", "v3"])
def test_read_old_lock_and_write_new(tmp_path: pathlib.Path, lockfile):
    # v1 lockfiles stored by a coarse DAG hash that did not include build deps.
    # They could not represent multiple build deps with different build hashes.
    #
    # v2 and v3 lockfiles are keyed by a "build hash", so they can represent specs
    # with different build deps but the same DAG hash. However, those two specs
    # could never have been built together, because they cannot coexist in a
    # Spack DB, which is keyed by DAG hash. The second one would just be a no-op
    # no-op because its DAG hash was already in the DB.
    #
    # Newer Spack uses a fine-grained DAG hash that includes build deps, package hash,
    # and more. But, we still have to identify old specs by their original DAG hash.
    # Essentially, the name (hash) we give something in Spack at concretization time is
    # its name forever (otherwise we'd need to relocate prefixes and disrupt existing
    # installations). So, we just discard the second conflicting dtbuild1 version when
    # reading v2 and v3 lockfiles. This is what old Spack would've done when installing
    # the environment, anyway.
    #
    # This test ensures the behavior described above.
    lockfile_path = os.path.join(spack.paths.test_path, "data", "legacy_env", "%s.lock" % lockfile)

    # read in the JSON from a legacy lockfile
    with open(lockfile_path, encoding="utf-8") as f:
        old_dict = sjson.load(f)

    # read all DAG hashes from the legacy lockfile and record its shadowed DAG hash.
    old_hashes = set()
    shadowed_hash = None
    for key, spec_dict in old_dict["concrete_specs"].items():
        if "hash" not in spec_dict:
            # v1 and v2 key specs by their name in concrete_specs
            name, spec_dict = next(iter(spec_dict.items()))
        else:
            # v3 lockfiles have a `name` field and key by hash
            name = spec_dict["name"]

        # v1 lockfiles do not have a "hash" field -- they use the key.
        dag_hash = key if lockfile == "v1" else spec_dict["hash"]
        old_hashes.add(dag_hash)

        # v1 lockfiles can't store duplicate build dependencies, so they
        # will not have a shadowed hash.
        if lockfile != "v1":
            # v2 and v3 lockfiles store specs by build hash, so they can have multiple
            # keys for the same DAG hash. We discard the second one (dtbuild@1.0).
            if name == "dtbuild1" and spec_dict["version"] == "1.0":
                shadowed_hash = dag_hash

    # make an env out of the old lockfile -- env should be able to read v1/v2/v3
    test_lockfile_path = str(tmp_path / "spack.lock")
    shutil.copy(lockfile_path, test_lockfile_path)
    _env_create("test", init_file=test_lockfile_path, with_view=False)

    # re-read the old env as a new lockfile
    e = ev.read("test")
    hashes = set(e._to_lockfile_dict()["concrete_specs"])

    # v1 doesn't have duplicate build deps.
    # in v2 and v3, the shadowed hash will be gone.
    if shadowed_hash:
        old_hashes -= set([shadowed_hash])

    # make sure we see the same hashes in old and new lockfiles
    assert old_hashes == hashes


def test_read_v1_lock_creates_backup(tmp_path: pathlib.Path):
    """When reading a version-1 lockfile, make sure that a backup of that file
    is created.
    """
    v1_lockfile_path = pathlib.Path(spack.paths.test_path) / "data" / "legacy_env" / "v1.lock"
    test_lockfile_path = tmp_path / "init" / ev.lockfile_name
    test_lockfile_path.parent.mkdir(parents=True, exist_ok=False)
    shutil.copy(v1_lockfile_path, test_lockfile_path)

    e = ev.create_in_dir(tmp_path, init_file=test_lockfile_path)
    assert os.path.exists(e._lock_backup_v1_path)
    assert filecmp.cmp(e._lock_backup_v1_path, v1_lockfile_path)


@pytest.mark.parametrize("lockfile", ["v1", "v2", "v3"])
def test_read_legacy_lockfile_and_reconcretize(
    mock_stage, mock_fetch, install_mockery, lockfile, tmp_path: pathlib.Path
):
    # In legacy lockfiles v2 and v3 (keyed by build hash), there may be multiple
    # versions of the same spec with different build dependencies, which means
    # they will have different build hashes but the same DAG hash.
    # In the case of DAG hash conflicts, we always keep the spec associated with
    # whichever root spec came first in the "roots" list.
    #
    # After reconcretization with the *new*, finer-grained DAG hash, there should no
    # longer be conflicts, and the previously conflicting specs can coexist in the
    # same environment.
    test_path = pathlib.Path(spack.paths.test_path)
    lockfile_content = test_path / "data" / "legacy_env" / f"{lockfile}.lock"
    legacy_lockfile_path = tmp_path / ev.lockfile_name
    shutil.copy(lockfile_content, legacy_lockfile_path)

    # The order of the root specs in this environment is:
    #     [
    #         wci7a3a -> dttop ^dtbuild1@0.5,
    #         5zg6wxw -> dttop ^dtbuild1@1.0
    #     ]
    # So in v2 and v3 lockfiles we have two versions of dttop with the same DAG
    # hash but different build hashes.

    env("create", "test", str(legacy_lockfile_path))
    test = ev.read("test")
    assert len(test.specs_by_hash) == 1

    single_root = next(iter(test.specs_by_hash.values()))

    # v1 only has version 1.0, because v1 was keyed by DAG hash, and v1.0 overwrote
    # v0.5 on lockfile creation. v2 only has v0.5, because we specifically prefer
    # the one that would be installed when we read old lockfiles.
    if lockfile == "v1":
        assert single_root["dtbuild1"].version == Version("1.0")
    else:
        assert single_root["dtbuild1"].version == Version("0.5")

    # Now forcefully reconcretize
    with ev.read("test"):
        concretize("-f")

    # After reconcretizing, we should again see two roots, one depending on each
    # of the dtbuild1 versions specified in the roots of the original lockfile.
    test = ev.read("test")
    assert len(test.specs_by_hash) == 2

    expected_versions = set([Version("0.5"), Version("1.0")])
    current_versions = set(s["dtbuild1"].version for s in test.specs_by_hash.values())
    assert current_versions == expected_versions


def _parse_dry_run_package_installs(make_output):
    """Parse `spack install ... # <spec>` output from a make dry run."""
    return [
        Spec(line.split("# ")[1]).name
        for line in make_output.splitlines()
        if line.startswith("spack")
    ]


@pytest.mark.parametrize(
    "depfile_flags,expected_installs",
    [
        # This installs the full environment
        (
            ["--use-buildcache=never"],
            [
                "dtbuild1",
                "dtbuild2",
                "dtbuild3",
                "dtlink1",
                "dtlink2",
                "dtlink3",
                "dtlink4",
                "dtlink5",
                "dtrun1",
                "dtrun2",
                "dtrun3",
                "dttop",
            ],
        ),
        # This prunes build deps at depth > 0
        (
            ["--use-buildcache=package:never,dependencies:only"],
            [
                "dtbuild1",
                "dtlink1",
                "dtlink2",
                "dtlink3",
                "dtlink4",
                "dtlink5",
                "dtrun1",
                "dtrun2",
                "dtrun3",
                "dttop",
            ],
        ),
        # This prunes all build deps
        (
            ["--use-buildcache=only"],
            ["dtlink1", "dtlink3", "dtlink4", "dtlink5", "dtrun1", "dtrun3", "dttop"],
        ),
        # Test whether pruning of build deps is correct if we explicitly include one
        # that is also a dependency of a root.
        (
            ["--use-buildcache=only", "dttop", "dtbuild1"],
            [
                "dtbuild1",
                "dtlink1",
                "dtlink2",
                "dtlink3",
                "dtlink4",
                "dtlink5",
                "dtrun1",
                "dtrun2",
                "dtrun3",
                "dttop",
            ],
        ),
    ],
)
def test_environment_depfile_makefile(
    depfile_flags, expected_installs, tmp_path: pathlib.Path, mock_packages
):
    env("create", "test")
    make = Executable("make")
    makefile = str(tmp_path / "Makefile")
    with ev.read("test"):
        add("dttop")
        concretize()

    # Disable jobserver so we can do a dry run.
    with ev.read("test"):
        env(
            "depfile",
            "-o",
            makefile,
            "--make-disable-jobserver",
            "--make-prefix=prefix",
            *depfile_flags,
        )

    # Do make dry run.
    out = make("-n", "-f", makefile, "SPACK=spack", output=str)

    specs_that_make_would_install = _parse_dry_run_package_installs(out)

    # Check that all specs are there (without duplicates)
    assert set(specs_that_make_would_install) == set(expected_installs)
    assert len(specs_that_make_would_install) == len(set(specs_that_make_would_install))


def test_depfile_safe_format():
    """Test that depfile.MakefileSpec.safe_format() escapes target names."""

    class SpecLike:
        def format(self, _):
            return "abc@def=ghi"

    spec = depfile.MakefileSpec(SpecLike())
    assert spec.safe_format("{name}") == "abc_def_ghi"
    assert spec.unsafe_format("{name}") == "abc@def=ghi"


def test_depfile_works_with_gitversions(tmp_path: pathlib.Path, mock_packages, monkeypatch):
    """Git versions may contain = chars, which should be escaped in targets,
    otherwise they're interpreted as makefile variable assignments."""
    monkeypatch.setattr(spack.package_base.PackageBase, "git", "repo.git", raising=False)
    env("create", "test")

    make = Executable("make")
    makefile = str(tmp_path / "Makefile")

    # Create an environment with dttop and dtlink1 both at a git version,
    # and generate a depfile
    with ev.read("test"):
        add(f"dttop@{'a' * 40}=1.0 ^dtlink1@{'b' * 40}=1.0")
        concretize()
        env("depfile", "-o", makefile, "--make-disable-jobserver", "--make-prefix=prefix")

    # Do a dry run on the generated depfile
    out = make("-n", "-f", makefile, "SPACK=spack", output=str)

    # Check that all specs are there (without duplicates)
    specs_that_make_would_install = _parse_dry_run_package_installs(out)
    expected_installs = [
        "dtbuild1",
        "dtbuild2",
        "dtbuild3",
        "dtlink1",
        "dtlink2",
        "dtlink3",
        "dtlink4",
        "dtlink5",
        "dtrun1",
        "dtrun2",
        "dtrun3",
        "dttop",
    ]
    assert set(specs_that_make_would_install) == set(expected_installs)
    assert len(specs_that_make_would_install) == len(set(specs_that_make_would_install))


@pytest.mark.parametrize(
    "picked_package,expected_installs",
    [
        (
            "dttop",
            [
                "dtbuild2",
                "dtlink2",
                "dtrun2",
                "dtbuild1",
                "dtlink4",
                "dtlink3",
                "dtlink1",
                "dtlink5",
                "dtbuild3",
                "dtrun3",
                "dtrun1",
                "dttop",
            ],
        ),
        ("dtrun1", ["dtlink5", "dtbuild3", "dtrun3", "dtrun1"]),
    ],
)
def test_depfile_phony_convenience_targets(
    picked_package, expected_installs: set, tmp_path: pathlib.Path, mock_packages
):
    """Check whether convenience targets "install/%" and "install-deps/%" are created for
    each package if "--make-prefix" is absent."""
    make = Executable("make")
    with fs.working_dir(str(tmp_path)):
        with ev.create_in_dir("."):
            add("dttop")
            concretize()

        with ev.Environment(".") as e:
            picked_spec = e.matching_spec(picked_package)
            env("depfile", "-o", "Makefile", "--make-disable-jobserver")

        # Phony install/* target should install picked package and all its deps
        specs_that_make_would_install = _parse_dry_run_package_installs(
            make(
                "-n",
                picked_spec.format("install/{name}-{version}-{hash}"),
                "SPACK=spack",
                output=str,
            )
        )

        assert set(specs_that_make_would_install) == set(expected_installs)
        assert len(specs_that_make_would_install) == len(set(specs_that_make_would_install))

        # Phony install-deps/* target shouldn't install picked package
        specs_that_make_would_install = _parse_dry_run_package_installs(
            make(
                "-n",
                picked_spec.format("install-deps/{name}-{version}-{hash}"),
                "SPACK=spack",
                output=str,
            )
        )

        assert set(specs_that_make_would_install) == set(expected_installs) - {picked_package}
        assert len(specs_that_make_would_install) == len(set(specs_that_make_would_install))


def test_environment_depfile_out(tmp_path: pathlib.Path, mock_packages):
    env("create", "test")
    makefile_path = str(tmp_path / "Makefile")
    with ev.read("test"):
        add("libdwarf")
        concretize()
    with ev.read("test"):
        env("depfile", "-G", "make", "-o", makefile_path)
        stdout = env("depfile", "-G", "make")
        with open(makefile_path, "r", encoding="utf-8") as f:
            assert stdout == f.read()


def test_spack_package_ids_variable(tmp_path: pathlib.Path, mock_packages):
    # Integration test for post-install hooks through prefix/SPACK_PACKAGE_IDS
    # variable
    env("create", "test")
    makefile_path = str(tmp_path / "Makefile")
    include_path = str(tmp_path / "include.mk")

    # Create env and generate depfile in include.mk with prefix example/
    with ev.read("test"):
        add("libdwarf")
        concretize()

    with ev.read("test"):
        env(
            "depfile",
            "-G",
            "make",
            "--make-disable-jobserver",
            "--make-prefix=example",
            "-o",
            include_path,
        )

    # Include in Makefile and create target that depend on SPACK_PACKAGE_IDS
    with open(makefile_path, "w", encoding="utf-8") as f:
        f.write(
            """
all: post-install

include include.mk

example/post-install/%: example/install/%
\t$(info post-install: $(HASH)) # noqa: W191,E101

post-install: $(addprefix example/post-install/,$(example/SPACK_PACKAGE_IDS))
"""
        )
    make = Executable("make")

    # Do dry run.
    out = make("-n", "-C", str(tmp_path), "SPACK=spack", output=str)

    # post-install: <hash> should've been executed
    with ev.read("test") as test:
        for s in test.all_specs():
            assert "post-install: {}".format(s.dag_hash()) in out


def test_depfile_empty_does_not_error(tmp_path: pathlib.Path):
    # For empty environments Spack should create a depfile that does nothing
    make = Executable("make")
    makefile = str(tmp_path / "Makefile")

    env("create", "test")
    with ev.read("test"):
        env("depfile", "-o", makefile)

    make("-f", makefile)

    assert make.returncode == 0


def test_unify_when_possible_works_around_conflicts(mutable_config):
    with ev.create("coconcretization") as e:
        mutable_config.set("concretizer:unify", "when_possible")
        e.add("mpileaks+opt")
        e.add("mpileaks~opt")
        e.add("mpich")
        e.concretize()

        assert len([x for x in e.all_specs() if x.satisfies("mpileaks")]) == 2
        assert len([x for x in e.all_specs() if x.satisfies("mpileaks+opt")]) == 1
        assert len([x for x in e.all_specs() if x.satisfies("mpileaks~opt")]) == 1
        assert len([x for x in e.all_specs() if x.satisfies("mpich")]) == 1


def test_env_include_packages_url(
    tmp_path: pathlib.Path, mutable_empty_config, mock_fetch_url_text, mock_curl_configs
):
    """Test inclusion of a (GitHub) URL."""
    develop_url = "https://github.com/fake/fake/blob/develop/"
    default_packages = develop_url + "etc/fake/defaults/packages.yaml"
    sha256 = "8d428c600b215e3b4a207a08236659dfc2c9ae2782c35943a00ee4204a135702"
    spack_yaml = tmp_path / "spack.yaml"
    with open(spack_yaml, "w", encoding="utf-8") as f:
        f.write(
            f"""\
spack:
  include:
  - path: {default_packages}
    sha256: {sha256}
"""
        )

    with spack.config.override("config:url_fetch_method", "curl"):
        env = ev.Environment(str(tmp_path))
        ev.activate(env)

        # Make sure a setting from test/data/config/packages.yaml is present
        cfg = spack.config.get("packages")
        assert "mpich" in cfg["all"]["providers"]["mpi"]


def test_relative_view_path_on_command_line_is_made_absolute(tmp_path: pathlib.Path):
    with fs.working_dir(str(tmp_path)):
        env("create", "--with-view", "view", "--dir", "env")
        environment = ev.Environment(os.path.join(".", "env"))
        environment.regenerate_views()
        assert os.path.samefile("view", environment.default_view.root)


def test_environment_created_in_users_location(mutable_mock_env_path, tmp_path: pathlib.Path):
    """Test that an environment is created in a location based on the config"""
    env_dir = str(mutable_mock_env_path)

    assert str(tmp_path) in env_dir
    assert not os.path.isdir(env_dir)

    dir_name = "user_env"
    env("create", dir_name)
    out = env("list")

    assert dir_name in out
    assert env_dir in ev.root(dir_name)
    assert os.path.isdir(os.path.join(env_dir, dir_name))


def test_environment_created_from_lockfile_has_view(
    mock_packages, temporary_store, tmp_path: pathlib.Path
):
    """When an env is created from a lockfile, a view should be generated for it"""
    env_a = str(tmp_path / "a")
    env_b = str(tmp_path / "b")

    # Create an environment and install a package in it
    env("create", "-d", env_a)
    with ev.Environment(env_a):
        add("libelf")
        install("--fake")

    # Create another environment from the lockfile of the first environment
    env("create", "-d", env_b, os.path.join(env_a, "spack.lock"))

    # Make sure the view was created
    with ev.Environment(env_b) as e:
        assert os.path.isdir(e.view_path_default)


def test_env_view_disabled(tmp_path: pathlib.Path, mutable_mock_env_path):
    """Ensure an inlined view being disabled means not even the default view
    is created (since the case doesn't appear to be covered in this module)."""
    spack_yaml = tmp_path / ev.manifest_name
    spack_yaml.write_text(
        """\
spack:
  specs:
  - mpileaks
  view: false
"""
    )
    env("create", "disabled", str(spack_yaml))
    with ev.read("disabled") as e:
        e.concretize()

    assert len(e.views) == 0
    assert not os.path.exists(e.view_path_default)


@pytest.mark.parametrize("first", ["false", "true", "custom"])
def test_env_include_mixed_views(
    tmp_path: pathlib.Path, mutable_config, mutable_mock_env_path, first
):
    """Ensure including path and boolean views in different combinations result
    in the creation of only the first view if it is not disabled."""
    false_yaml = tmp_path / "false-view.yaml"
    false_yaml.write_text("view: false\n")

    true_yaml = tmp_path / "true-view.yaml"
    true_yaml.write_text("view: true\n")

    custom_name = "my-test-view"
    custom_view = tmp_path / custom_name
    custom_yaml = tmp_path / "custom-view.yaml"
    custom_yaml.write_text(
        f"""
view:
  {custom_name}:
    root: {custom_view}
"""
    )

    if first == "false":
        order = [false_yaml, true_yaml, custom_yaml]
    elif first == "true":
        order = [true_yaml, custom_yaml, false_yaml]
    else:
        order = [custom_yaml, false_yaml, true_yaml]
    includes = [f"  - {yaml}\n" for yaml in order]

    spack_yaml = tmp_path / ev.manifest_name
    spack_yaml.write_text(
        f"""\
spack:
  include:
{"".join(includes)}
  specs:
  - mpileaks
"""
    )

    env("create", "test", str(spack_yaml))
    with ev.read("test") as e:
        concretize()

    # Only the first included view should be created if view not disabled by it
    assert len(e.views) == 0 if first == "false" else 1
    if first == "true":
        assert os.path.exists(e.view_path_default)
    else:
        assert not os.path.exists(e.view_path_default)

    if first == "custom":
        assert os.path.exists(custom_view)
    else:
        assert not os.path.exists(custom_view)


def test_stack_view_multiple_views_same_name(
    installed_environment, template_combinatorial_env, tmp_path: pathlib.Path
):
    """Test multiple views with the same name combine settings with precedence
    given to the options in spack.yaml."""
    # Write the view configuration and or manifest file

    view_filename = tmp_path / "view.yaml"
    default_dir = tmp_path / "default-view"
    default_view = f"""\
view:
  default:
    root: {default_dir}
    select: ['target=x86_64']
    projections:
      all: '{{architecture.target}}/{{name}}-{{version}}-from-view'
"""
    view_filename.write_text(default_view)

    view_dir = tmp_path / "view"
    with installed_environment(
        f"""\
spack:
  include:
  - {view_filename}
  definitions:
    - packages: [mpileaks, cmake]
    - targets: ['target=x86_64', 'target=core2']

  specs:
    - matrix:
        - [$packages]
        - [$targets]

  view:
    default:
      root: {view_dir}
      exclude: ['cmake']
      projections:
        all: '{{architecture.target}}/{{name}}-{{version}}'
"""
    ) as e:
        # the view root in the included view should NOT exist
        assert not os.path.exists(str(default_dir))

        for spec in traverse_nodes(e.concrete_roots(), deptype=("link", "run")):
            # no specs will exist in the included view projection
            base_dir = view_dir / f"{spec.architecture.target}"
            included_dir = base_dir / f"{spec.name}-{spec.version}-from-view"
            assert not included_dir.exists()

            # only target=x86_64 specs (selected in the included view) that
            # are also not cmake (excluded in the environment view) should exist
            if spec.name == "gcc-runtime":
                continue
            current_dir = view_dir / f"{spec.architecture.target}" / f"{spec.name}-{spec.version}"
            assert current_dir.exists() is not (
                spec.satisfies("cmake") or spec.satisfies("target=core2")
            )


def test_env_view_resolves_identical_file_conflicts(
    tmp_path: pathlib.Path, install_mockery, mock_fetch
):
    """When files clash in a view, but refer to the same file on disk (for example, the dependent
    symlinks to a file in the dependency at the same relative path), Spack links the first regular
    file instead of symlinks. This is important for copy type views where we need the underlying
    file to be copied instead of the symlink (when a symlink would be copied, it would become a
    self-referencing symlink after relocation). The test uses a symlink type view though, since
    that keeps track of the original file path."""
    with ev.create("env", with_view=tmp_path / "view") as e:
        add("view-resolve-conflict-top")
        install()
        top = e.matching_spec("view-resolve-conflict-top").prefix
        bottom = e.matching_spec("view-file").prefix

    # In this example we have `./bin/x` in 3 prefixes, two links, one regular file. We expect the
    # regular file to be linked into the view. There are also 2 links at `./bin/y`, but no regular
    # file, so we expect standard behavior: first entry is linked into the view.

    #   view-resolve-conflict-top/bin/
    #     x -> view-file/bin/x
    #     y -> view-resolve-conflict-middle/bin/y    # expect this y to be linked
    #   view-resolve-conflict-middle/bin/
    #     x -> view-file/bin/x
    #     y -> view-file/bin/x
    #   view-file/bin/
    #     x                                          # expect this x to be linked

    assert readlink(tmp_path / "view" / "bin" / "x") == bottom.bin.x
    assert readlink(tmp_path / "view" / "bin" / "y") == top.bin.y


def test_env_view_ignores_different_file_conflicts(
    tmp_path: pathlib.Path, install_mockery, mock_fetch
):
    """Test that file-file conflicts for two unique files in environment views are ignored, and
    that the dependent's file is linked into the view, not the dependency's file."""
    with ev.create("env", with_view=tmp_path / "view") as e:
        add("view-ignore-conflict")
        install()
        prefix_dependent = e.matching_spec("view-ignore-conflict").prefix
    # The dependent's file is linked into the view
    assert readlink(tmp_path / "view" / "bin" / "x") == prefix_dependent.bin.x


@pytest.mark.regression("51054")
def test_non_str_repos(installed_environment):
    with installed_environment(
        """\
spack:
  repos:
    builtin:
      branch: develop"""
    ):
        pass


def test_concretized_specs_and_include_concrete(mutable_config):
    """Tests the consistency of concretized specs when there are either
    duplicate input specs or duplicate hashes.
    """
    # Create a structure like this one
    #
    # Local specs:
    # - mpileaks -> hash1
    # - libelf@0.8.12 -> hash2
    # - pkg-a -> hash3
    #
    # Included specs:
    # - mpileaks -> hash4
    # - libelf -> hash2
    # - pkg-a -> hash3
    env("create", "included-env")
    with ev.read("included-env") as e:
        e.add("mpileaks")
        e.add("libelf")
        e.add("pkg-a")
        mutable_config.set(
            "packages", {"mpileaks": {"require": ["@2.2"]}, "libelf": {"require": ["@0.8.12"]}}
        )
        included_pairs = e.concretize()
        e.write()

    env("create", "--include-concrete", "included-env", "main-env")
    with ev.read("main-env") as e:
        e.add("mpileaks")
        e.add("libelf@0.8.12")
        e.add("pkg-a")
        mutable_config.set("packages", {"mpileaks": {"require": ["@2.3"]}})
        spec_pairs = e.concretize()
        concretized_specs = list(e.concretized_specs())
        assert list(dedupe(spec_pairs + included_pairs)) == concretized_specs
        assert len(concretized_specs) == 5


def test_view_can_select_group_of_specs(installed_environment, tmp_path: pathlib.Path):
    """Tests that we can select groups of specs in a view and exclude other groups"""
    view_dir = tmp_path / "view"
    with installed_environment(
        f"""\
spack:
  specs:
    - group: apps1
      specs:
      - mpileaks
    - group: apps2
      specs:
      - cmake
    - group: apps3
      specs:
      - pkg-a
  view:
    default:
      root: {view_dir}
      group: [apps1, apps2]
"""
    ) as test:
        for item in test.concretized_roots:
            # Assertions are based on the behavior of the "--fake" install
            bin_file = pathlib.Path(test.default_view.view()._root) / "bin" / item.root.name
            assert not bin_file.exists() if item.group == "apps3" else bin_file.exists()


def test_view_can_select_group_of_specs_using_string(
    installed_environment, tmp_path: pathlib.Path
):
    """Tests that we can select groups of specs in a view and exclude other groups"""
    view_dir = tmp_path / "view"
    with installed_environment(
        f"""\
spack:
  specs:
    - group: apps1
      specs:
      - mpileaks
    - group: apps2
      specs:
      - cmake
  view:
    default:
      root: {view_dir}
      group: apps1
"""
    ) as test:
        for item in test.concretized_roots:
            # Assertions are based on the behavior of the "--fake" install
            bin_file = pathlib.Path(test.default_view.view()._root) / "bin" / item.root.name
            assert not bin_file.exists() if item.group == "apps2" else bin_file.exists()


def test_env_include_concrete_only(tmp_path, mock_packages, mutable_config):
    """Confirm that an environment that only includes a concrete environment actually loads it."""
    specs = ["libdwarf", "libelf"]

    include_dir = tmp_path / "includes"
    include_dir.mkdir()
    include_manifest = include_dir / ev.manifest_name
    include_manifest.write_text(
        f"""\
spack:
  specs:
  - {specs[0]}
  - {specs[1]}
"""
    )
    include_env = ev.create("test_include", include_manifest)
    include_env.concretize()
    include_env.write()

    include_lockfile = include_env.lock_path
    assert os.path.exists(include_lockfile)

    manifest_file = tmp_path / ev.manifest_name
    manifest_file.write_text(
        f"""\
spack:
  include:
  - {str(include_lockfile)}
"""
    )
    e = ev.create("test", manifest_file)

    # Confirm the only specs the environment has are those loaded from the
    # lockfile.
    assert len(e.user_specs) == 0
    all_concrete = [s for s, _ in e.concretized_specs()]
    for spec in specs:
        assert Spec(spec) in all_concrete


@pytest.mark.parametrize(
    "concrete,includes",
    [
        (["$HOME/path/to/other/environment"], []),
        (["$HOME/path/to/another/environment"], ["a/b", "$HOME/includes"]),
    ],
)
def test_env_update_include_concrete(tmp_path: pathlib.Path, concrete, includes):
    """Confirm update of include_concrete converts it to include."""

    config = {"include_concrete": concrete}
    if includes:
        config["include"] = includes
    new_concrete = [os.path.join(p, ev.lockfile_name) for p in concrete]
    assert spack.schema.env.update(config)
    assert "include_concrete" not in config
    assert config["include"] == new_concrete + includes


def test_include_concrete_deprecation_warning(
    tmp_path: pathlib.Path, environment_from_manifest, capfd
):
    try:
        environment_from_manifest(
            """\
spack:
  include_concrete:
  - /path/to/some/environment
"""
        )
    except ev.SpackEnvironmentError:
        pass

    _, err = capfd.readouterr()
    assert "should be 'include'" in err


def test_env_include_concrete_relative_path(tmp_path, mock_packages, mutable_config):
    """Tests that a relative path in 'include' for a spack.lock is resolved relative to the
    manifest file, not the current working directory.
    """
    # Create and concretize the included environment.
    include_dir = tmp_path / "include_env"
    include_dir.mkdir()
    (include_dir / ev.manifest_name).write_text(
        """\
spack:
  specs:
  - libdwarf
"""
    )
    with ev.Environment(str(include_dir)) as e:
        e.concretize()
        e.write()
        assert os.path.exists(e.lock_path)

    # Create the main environment in a sibling directory, using a *relative* path
    main_dir = tmp_path / "main_env"
    main_dir.mkdir()
    relative_lockfile = f"../include_env/{ev.lockfile_name}"
    (main_dir / ev.manifest_name).write_text(
        f"""\
spack:
  include:
  - {relative_lockfile}
"""
    )
    with ev.Environment(str(main_dir)) as e:
        e.concretize()
        e.write()
        assert len(e.user_specs) == 0
        assert [s for s, _ in e.concretized_specs()] == [Spec("libdwarf")]


def test_env_include_concrete_git_lockfile(tmp_path, mock_packages, mutable_config, monkeypatch):
    """Tests that a spack.lock listed inside a git-based include is resolved using the
    clone destination as the base, not the manifest directory.
    """
    # Create and concretize the included environment.
    include_dir = tmp_path / "include_env"
    include_dir.mkdir()
    (include_dir / ev.manifest_name).write_text(
        """\
spack:
  specs:
  - libdwarf
"""
    )
    with ev.Environment(str(include_dir)) as e:
        e.concretize()
        e.write()
        assert os.path.exists(e.lock_path)

        # Simulate a cloned git repo: the spack.lock lives at a subpath within the clone.
        clone_dest = tmp_path / "git_clone"
        lock_subpath = "envs/staging/spack.lock"
        lock_in_clone = clone_dest / "envs" / "staging" / ev.lockfile_name
        lock_in_clone.parent.mkdir(parents=True)
        shutil.copy(e.lock_path, lock_in_clone)
        # is_env_dir() requires spack.yaml alongside spack.lock
        shutil.copy(os.path.join(e.path, ev.manifest_name), lock_in_clone.parent)

    # Prevent actual git operations; return the pre-built clone destination.
    monkeypatch.setattr(
        spack.config.GitIncludePaths, "_clone", lambda self, parent_scope: str(clone_dest)
    )

    main_dir = tmp_path / "main_env"
    main_dir.mkdir()
    (main_dir / ev.manifest_name).write_text(
        f"""\
spack:
  include:
  - git: https://example.com/configs.git
    branch: main
    paths:
    - {lock_subpath}
"""
    )
    with ev.Environment(str(main_dir)) as e:
        e.concretize()
        e.write()
        assert len(e.user_specs) == 0
        assert [s for s, _ in e.concretized_specs()] == [Spec("libdwarf")]


@pytest.mark.skipif(sys.platform != "linux", reason="Target is linux-specific")
def test_compiler_target_env(mock_packages, environment_from_manifest):
    """Tests that Spack doesn't drop flag definitions on compilers
    when a target is required in config.
    """

    cflags = "-Wall"
    env = environment_from_manifest(
        f"""\
spack:
  specs:
  - libdwarf %c=gcc@12.100.100
  packages:
    all:
      require:
      - "target=x86_64_v3"
    gcc:
      externals:
      - spec: gcc@12.100.100 languages:=c,c++
        prefix: /fake
        extra_attributes:
          compilers:
            c: /fake/bin/gcc
            cxx: /fake/bin/g++
          flags:
            cflags: {cflags}
      require: "gcc"
"""
    )

    with env:
        env.concretize()
        libdwarf = env.concrete_roots()[0]
        assert libdwarf.satisfies("cflags=-Wall")
        # Sanity check: make sure the target we expect was applied to the
        # compiler entry
        assert libdwarf["c"].satisfies("gcc@12.100.100 languages:=c,c++ target=x86_64_v3")


@pytest.mark.regression("52247")
def test_create_with_orphaned_directory(mutable_mock_env_path: pathlib.Path):
    """Tests that an orphaned environment directory (directory exists, no spack.yaml) must not
    prevent 'spack env create' from creating a new environment with that name.
    """
    orphaned = mutable_mock_env_path / "test1"
    orphaned_subdir = orphaned / ".spack-env"
    orphaned_subdir.mkdir(parents=True)

    # The orphaned directory must not be seen as an existing environment
    assert not ev.exists("test1")

    # Creating an environment over an orphaned directory must succeed
    env("create", "test1")

    assert ev.exists("test1")
    assert "test1" in env("list")


@pytest.mark.parametrize(
    "setup",
    [
        # valid environment: spack.yaml is a regular file
        pytest.param("valid", id="valid"),
        # orphaned directory: no spack.yaml at all
        pytest.param("orphaned", id="orphaned"),
        # broken manifest symlink: spack.yaml points to a non-existent target
        pytest.param("broken_symlink", id="broken_symlink"),
    ],
)
@pytest.mark.regression("52247")
def test_exists_consistent_with_all_environment_names(
    mutable_mock_env_path: pathlib.Path, setup: str
):
    """Tests that exists() and all_environment_names() agree on whether an environment exists."""
    env_dir = mutable_mock_env_path / "myenv"
    env_dir.mkdir(parents=True)
    manifest = env_dir / ev.manifest_name

    if setup == "valid":
        manifest.write_text(ev.default_manifest_yaml())
    elif setup == "orphaned":
        pass  # no manifest
    elif setup == "broken_symlink":
        manifest.symlink_to("/nonexistent/spack.yaml")

    listed = "myenv" in ev.all_environment_names()
    assert ev.exists("myenv") == listed
