"""Tests for Runtime class."""

# pylint: disable=protected-access
from __future__ import annotations

import logging
import os
import pathlib
import subprocess
from contextlib import contextmanager
from pathlib import Path
from shutil import rmtree
from typing import TYPE_CHECKING, Any

import pytest
from packaging.version import Version

from ansible_compat.config import ansible_version
from ansible_compat.constants import INVALID_PREREQUISITES_RC
from ansible_compat.errors import (
    AnsibleCommandError,
    AnsibleCompatError,
    InvalidPrerequisiteError,
)
from ansible_compat.runtime import (
    CompletedProcess,
    Runtime,
    _get_galaxy_role_name,
    is_url,
    search_galaxy_paths,
)

if TYPE_CHECKING:
    from collections.abc import Iterator

    from _pytest.monkeypatch import MonkeyPatch
    from pytest_mock import MockerFixture


def test_runtime_version(runtime: Runtime) -> None:
    """Tests version property."""
    version = runtime.version
    assert isinstance(version, Version)
    # tests that caching property value worked (coverage)
    assert version == runtime.version


@pytest.mark.parametrize(
    "require_module",
    (True, False),
    ids=("module-required", "module-unrequired"),
)
def test_runtime_version_outdated(require_module: bool) -> None:
    """Checks that instantiation raises if version is outdated."""
    with pytest.raises(RuntimeError, match="Found incompatible version of ansible"):
        Runtime(min_required_version="9999.9.9", require_module=require_module)


def test_runtime_missing_ansible_module(monkeypatch: MonkeyPatch) -> None:
    """Checks that we produce a RuntimeError when ansible module is missing."""

    class RaiseException:
        """Class to raise an exception."""

        def __init__(
            self,
            *args: Any,  # noqa: ARG002,ANN401
            **kwargs: Any,  # noqa: ARG002,ANN401
        ) -> None:
            raise ModuleNotFoundError

    monkeypatch.setattr("importlib.import_module", RaiseException)

    with pytest.raises(RuntimeError, match="Unable to find Ansible python module."):
        Runtime(require_module=True)


def test_runtime_mismatch_ansible_module(monkeypatch: MonkeyPatch) -> None:
    """Test that missing module is detected."""
    monkeypatch.setattr("ansible.release.__version__", "0.0.0", raising=False)
    with pytest.raises(RuntimeError, match="versions do not match"):
        Runtime(require_module=True)


def test_runtime_require_module() -> None:
    """Check that require_module successful pass."""
    Runtime(require_module=True)
    # Now we try to set the collection path, something to check if that is
    # causing an exception, as 2.15 introduced new init code.
    from ansible.utils.collection_loader import (  # pylint: disable=import-outside-toplevel
        AnsibleCollectionConfig,
    )

    AnsibleCollectionConfig.playbook_paths = "."
    # Calling it again in order to see that it does not produce UserWarning: AnsibleCollectionFinder has already been configured
    # which is done by Ansible core 2.15+. We added special code inside Runtime
    # that should avoid initializing twice and raise that warning.
    Runtime(require_module=True)


def test_runtime_version_fail_module(mocker: MockerFixture) -> None:
    """Tests for failure to detect Ansible version."""
    patched = mocker.patch(
        "ansible_compat.runtime.parse_ansible_version",
        autospec=True,
    )
    patched.side_effect = InvalidPrerequisiteError(
        "Unable to parse ansible cli version",
    )
    runtime = Runtime()
    with pytest.raises(
        InvalidPrerequisiteError,
        match="Unable to parse ansible cli version",
    ):
        _ = runtime.version  # pylint: disable=pointless-statement


def test_runtime_version_fail_cli(mocker: MockerFixture) -> None:
    """Tests for failure to detect Ansible version."""
    mocker.patch(
        "ansible_compat.runtime.Runtime.run",
        return_value=CompletedProcess(
            ["x"],
            returncode=123,
            stdout="oops",
            stderr="some error",
        ),
        autospec=True,
    )
    runtime = Runtime()
    with pytest.raises(
        RuntimeError,
        match="Unable to find a working copy of ansible executable.",
    ):
        _ = runtime.version  # pylint: disable=pointless-statement


def test_runtime_prepare_ansible_paths_validation() -> None:
    """Check that we validate collection_path."""
    runtime = Runtime()
    runtime.config.collections_paths = "invalid-value"  # type: ignore[assignment]
    with pytest.raises(RuntimeError, match="Unexpected ansible configuration"):
        runtime._prepare_ansible_paths()


@pytest.mark.parametrize(
    ("folder", "role_name", "isolated"),
    (
        ("ansible-role-sample", "acme.sample", True),
        ("acme.sample2", "acme.sample2", True),
        ("sample3", "acme.sample3", True),
        ("sample4", "acme.sample4", False),
    ),
    ids=("1", "2", "3", "4"),
)
def test_runtime_install_role(
    caplog: pytest.LogCaptureFixture,
    folder: str,
    role_name: str,
    isolated: bool,
) -> None:
    """Checks that we can install roles."""
    caplog.set_level(logging.INFO)
    project_dir = Path(__file__).parent / "roles" / folder
    runtime = Runtime(isolated=isolated, project_dir=project_dir)
    runtime.prepare_environment(install_local=True)
    # check that role appears as installed now
    result = runtime.run(["ansible-galaxy", "list"])
    assert result.returncode == 0, result
    assert role_name in result.stdout
    if isolated:
        assert pathlib.Path(f"{runtime.cache_dir}/roles/{role_name}").is_symlink()
    else:
        assert pathlib.Path(
            f"{Path(runtime.config.default_roles_path[0]).expanduser()}/{role_name}",
        ).is_symlink()
    runtime.clean()
    # also test that clean does not break when cache_dir is missing
    tmp_dir = runtime.cache_dir
    runtime.cache_dir = None
    runtime.clean()
    runtime.cache_dir = tmp_dir


def test_prepare_environment_with_collections(tmp_path: pathlib.Path) -> None:
    """Check that collections are correctly installed."""
    runtime = Runtime(isolated=True, project_dir=tmp_path)
    runtime.prepare_environment(required_collections={"community.molecule": "0.1.0"})


def test_runtime_install_requirements_missing_file() -> None:
    """Check that missing requirements file is ignored."""
    # Do not rely on this behavior, it may be removed in the future
    runtime = Runtime()
    runtime.install_requirements(Path("/that/does/not/exist"))


@pytest.mark.parametrize(
    ("file", "exc", "msg"),
    (
        (
            Path("/dev/null"),
            InvalidPrerequisiteError,
            "file is not a valid Ansible requirements file",
        ),
        (
            Path(__file__).parent / "assets" / "requirements-invalid-collection.yml",
            AnsibleCommandError,
            "Got 1 exit code while running: ansible-galaxy",
        ),
        (
            Path(__file__).parent / "assets" / "requirements-invalid-role.yml",
            AnsibleCommandError,
            "Got 1 exit code while running: ansible-galaxy",
        ),
    ),
    ids=("empty", "invalid-collection", "invalid-role"),
)
def test_runtime_install_requirements_invalid_file(
    file: Path,
    exc: type[Any],
    msg: str,
) -> None:
    """Check that invalid requirements file is raising."""
    runtime = Runtime()
    with pytest.raises(
        exc,
        match=msg,
    ):
        runtime.install_requirements(file)


@contextmanager
def cwd(path: Path) -> Iterator[None]:
    """Context manager for temporary changing current working directory."""
    old_pwd = Path.cwd()
    os.chdir(path)
    try:
        yield
    finally:
        os.chdir(old_pwd)


def test_prerun_reqs_v1(caplog: pytest.LogCaptureFixture) -> None:
    """Checks that the linter can auto-install requirements v1 when found."""
    path = Path(__file__).parent.parent / "examples" / "reqs_v1"
    runtime = Runtime(project_dir=path, verbosity=1)
    with cwd(path):
        runtime.prepare_environment()
    assert any(
        msg.startswith("Running ansible-galaxy role install") for msg in caplog.messages
    )
    assert all(
        "Running ansible-galaxy collection install" not in msg
        for msg in caplog.messages
    )


def test_prerun_reqs_v2(caplog: pytest.LogCaptureFixture) -> None:
    """Checks that the linter can auto-install requirements v2 when found."""
    path = (Path(__file__).parent.parent / "examples" / "reqs_v2").resolve()
    runtime = Runtime(project_dir=path, verbosity=1)
    with cwd(path):
        runtime.prepare_environment()
        assert any(
            msg.startswith("Running ansible-galaxy role install")
            for msg in caplog.messages
        )
        assert any(
            msg.startswith("Running ansible-galaxy collection install")
            for msg in caplog.messages
        )


def test_prerun_reqs_broken() -> None:
    """Checks that the we report invalid requirements.yml file."""
    path = (Path(__file__).parent.parent / "examples" / "reqs_broken").resolve()
    runtime = Runtime(project_dir=path, verbosity=1)
    with cwd(path), pytest.raises(InvalidPrerequisiteError):
        runtime.prepare_environment()


def test__update_env_no_old_value_no_default_no_value(monkeypatch: MonkeyPatch) -> None:
    """Make sure empty value does not touch environment."""
    monkeypatch.delenv("DUMMY_VAR", raising=False)

    runtime = Runtime()
    runtime._update_env("DUMMY_VAR", [])

    assert "DUMMY_VAR" not in runtime.environ


def test__update_env_no_old_value_no_value(monkeypatch: MonkeyPatch) -> None:
    """Make sure empty value does not touch environment."""
    monkeypatch.delenv("DUMMY_VAR", raising=False)

    runtime = Runtime()
    runtime._update_env("DUMMY_VAR", [], "a:b")

    assert "DUMMY_VAR" not in runtime.environ


def test__update_env_no_default_no_value(monkeypatch: MonkeyPatch) -> None:
    """Make sure empty value does not touch environment."""
    monkeypatch.setenv("DUMMY_VAR", "a:b")

    runtime = Runtime()
    runtime._update_env("DUMMY_VAR", [])

    assert runtime.environ["DUMMY_VAR"] == "a:b"


@pytest.mark.parametrize(
    ("value", "result"),
    (
        (["a"], "a"),
        (["a", "b"], "a:b"),
        (["a", "b", "c"], "a:b:c"),
    ),
)
def test__update_env_no_old_value_no_default(
    monkeypatch: MonkeyPatch,
    value: list[str],
    result: str,
) -> None:
    """Values are concatenated using : as the separator."""
    monkeypatch.delenv("DUMMY_VAR", raising=False)

    runtime = Runtime()
    runtime._update_env("DUMMY_VAR", value)

    assert runtime.environ["DUMMY_VAR"] == result


@pytest.mark.parametrize(
    ("default", "value", "result"),
    (
        ("a:b", ["c"], "c:a:b"),
        ("a:b", ["c:d"], "c:d:a:b"),
    ),
)
def test__update_env_no_old_value(
    monkeypatch: MonkeyPatch,
    default: str,
    value: list[str],
    result: str,
) -> None:
    """Values are appended to default value."""
    monkeypatch.delenv("DUMMY_VAR", raising=False)

    runtime = Runtime()
    runtime._update_env("DUMMY_VAR", value, default)

    assert runtime.environ["DUMMY_VAR"] == result


@pytest.mark.parametrize(
    ("old_value", "value", "result"),
    (
        ("a:b", ["c"], "c:a:b"),
        ("a:b", ["c:d"], "c:d:a:b"),
    ),
)
def test__update_env_no_default(
    monkeypatch: MonkeyPatch,
    old_value: str,
    value: list[str],
    result: str,
) -> None:
    """Values are appended to preexisting value."""
    monkeypatch.setenv("DUMMY_VAR", old_value)

    runtime = Runtime()
    runtime._update_env("DUMMY_VAR", value)

    assert runtime.environ["DUMMY_VAR"] == result


@pytest.mark.parametrize(
    ("old_value", "default", "value", "result"),
    (
        ("", "", ["e"], "e"),
        ("a", "", ["e"], "e:a"),
        ("", "c", ["e"], "e"),
        ("a", "c", ["e:f"], "e:f:a"),
    ),
)
def test__update_env(
    monkeypatch: MonkeyPatch,
    old_value: str,
    default: str,  # pylint: disable=unused-argument # noqa: ARG001
    value: list[str],
    result: str,
) -> None:
    """Defaults are ignored when preexisting value is present."""
    monkeypatch.setenv("DUMMY_VAR", old_value)

    runtime = Runtime()
    runtime._update_env("DUMMY_VAR", value)

    assert runtime.environ["DUMMY_VAR"] == result


def test_require_collection_wrong_version(runtime: Runtime) -> None:
    """Tests behaviour of require_collection."""
    subprocess.check_output(
        [  # noqa: S603
            "ansible-galaxy",
            "collection",
            "install",
            "examples/reqs_v2/community-molecule-0.1.0.tar.gz",
            "-p",
            "~/.ansible/collections",
        ],
    )
    with pytest.raises(InvalidPrerequisiteError) as pytest_wrapped_e:
        runtime.require_collection("community.molecule", "9999.9.9")
    assert pytest_wrapped_e.type == InvalidPrerequisiteError
    assert pytest_wrapped_e.value.code == INVALID_PREREQUISITES_RC


def test_require_collection_invalid_name(runtime: Runtime) -> None:
    """Check that require_collection raise with invalid collection name."""
    with pytest.raises(
        InvalidPrerequisiteError,
        match="Invalid collection name supplied:",
    ):
        runtime.require_collection("that-is-invalid")


def test_require_collection_invalid_collections_path(runtime: Runtime) -> None:
    """Check that require_collection raise with invalid collections path."""
    runtime.config.collections_paths = "/that/is/invalid"  # type: ignore[assignment]
    with pytest.raises(
        InvalidPrerequisiteError,
        match="Unable to determine ansible collection paths",
    ):
        runtime.require_collection("community.molecule")


def test_require_collection_preexisting_broken(tmp_path: pathlib.Path) -> None:
    """Check that require_collection raise with broken pre-existing collection."""
    runtime = Runtime(isolated=True, project_dir=tmp_path)
    dest_path: str = runtime.config.collections_paths[0]
    dest = pathlib.Path(dest_path) / "ansible_collections" / "foo" / "bar"
    dest.mkdir(parents=True, exist_ok=True)
    with pytest.raises(InvalidPrerequisiteError, match="missing MANIFEST.json"):
        runtime.require_collection("foo.bar")


def test_require_collection(runtime_tmp: Runtime) -> None:
    """Check that require collection successful install case."""
    runtime_tmp.require_collection("community.molecule", "0.1.0")


@pytest.mark.parametrize(
    ("name", "version", "install"),
    (
        ("fake_namespace.fake_name", None, True),
        ("fake_namespace.fake_name", "9999.9.9", True),
        ("fake_namespace.fake_name", None, False),
    ),
    ids=("a", "b", "c"),
)
def test_require_collection_missing(
    name: str,
    version: str,
    install: bool,
    runtime: Runtime,
) -> None:
    """Tests behaviour of require_collection, missing case."""
    with pytest.raises(AnsibleCompatError) as pytest_wrapped_e:
        runtime.require_collection(name=name, version=version, install=install)
    assert pytest_wrapped_e.type == InvalidPrerequisiteError
    assert pytest_wrapped_e.value.code == INVALID_PREREQUISITES_RC


def test_install_collection(runtime: Runtime) -> None:
    """Check that valid collection installs do not fail."""
    runtime.install_collection("examples/reqs_v2/community-molecule-0.1.0.tar.gz")


def test_install_collection_git(runtime: Runtime) -> None:
    """Check that valid collection installs do not fail."""
    runtime.install_collection(
        "git+https://github.com/ansible-collections/ansible.posix,main",
    )


def test_install_collection_dest(runtime: Runtime, tmp_path: pathlib.Path) -> None:
    """Check that valid collection to custom destination passes."""
    # Since Ansible 2.15.3 there is no guarantee that this will install the collection at requested path
    # as it might decide to not install anything if requirement is already present at another location.
    runtime.install_collection(
        "examples/reqs_v2/community-molecule-0.1.0.tar.gz",
        destination=tmp_path,
    )
    runtime.load_collections()
    for collection in runtime.collections:
        if collection == "community.molecule":
            return
    msg = "Failed to find collection as installed."
    raise AssertionError(msg)


def test_install_collection_fail(runtime: Runtime) -> None:
    """Check that invalid collection install fails."""
    with pytest.raises(AnsibleCompatError) as pytest_wrapped_e:
        runtime.install_collection("community.molecule:>=9999.0")
    assert pytest_wrapped_e.type == InvalidPrerequisiteError
    assert pytest_wrapped_e.value.code == INVALID_PREREQUISITES_RC


def test_install_galaxy_role(runtime_tmp: Runtime) -> None:
    """Check install role with empty galaxy file."""
    pathlib.Path(f"{runtime_tmp.project_dir}/galaxy.yml").touch()
    pathlib.Path(f"{runtime_tmp.project_dir}/meta").mkdir()
    pathlib.Path(f"{runtime_tmp.project_dir}/meta/main.yml").touch()
    # this should only raise a warning
    runtime_tmp._install_galaxy_role(runtime_tmp.project_dir, role_name_check=1)
    # this should test the bypass role name check path
    runtime_tmp._install_galaxy_role(runtime_tmp.project_dir, role_name_check=2)
    # this should raise an error
    with pytest.raises(
        InvalidPrerequisiteError,
        match="does not follow current galaxy requirements",
    ):
        runtime_tmp._install_galaxy_role(runtime_tmp.project_dir, role_name_check=0)


def test_install_galaxy_role_unlink(
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Test ability to unlink incorrect symlinked roles."""
    runtime_tmp = Runtime(verbosity=1)
    runtime_tmp.prepare_environment()
    pathlib.Path(f"{runtime_tmp.cache_dir}/roles").mkdir(parents=True, exist_ok=True)
    pathlib.Path(f"{runtime_tmp.cache_dir}/roles/acme.get_rich").symlink_to("/dev/null")
    pathlib.Path(f"{runtime_tmp.project_dir}/meta").mkdir()
    pathlib.Path(f"{runtime_tmp.project_dir}/meta/main.yml").write_text(
        """galaxy_info:
  role_name: get_rich
  namespace: acme
""",
        encoding="utf-8",
    )
    runtime_tmp._install_galaxy_role(runtime_tmp.project_dir)
    assert "symlink to current repository" in caplog.text


def test_install_galaxy_role_bad_namespace(runtime_tmp: Runtime) -> None:
    """Check install role with bad namespace in galaxy info."""
    pathlib.Path(f"{runtime_tmp.project_dir}/meta").mkdir()
    pathlib.Path(f"{runtime_tmp.project_dir}/meta/main.yml").write_text(
        """galaxy_info:
  role_name: foo
  author: bar
  namespace: ["xxx"]
""",
    )
    # this should raise an error regardless the role_name_check value
    with pytest.raises(AnsibleCompatError, match="Role namespace must be string, not"):
        runtime_tmp._install_galaxy_role(runtime_tmp.project_dir, role_name_check=1)


@pytest.mark.parametrize(
    "galaxy_info",
    (
        """galaxy_info:
  role_name: foo-bar
  namespace: acme
""",
        """galaxy_info:
  role_name: foo-bar
""",
    ),
    ids=("bad-name", "bad-name-without-namespace"),
)
def test_install_galaxy_role_name_role_name_check_equals_to_1(
    runtime_tmp: Runtime,
    galaxy_info: str,
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Check install role with bad role name in galaxy info."""
    caplog.set_level(logging.WARNING)
    pathlib.Path(f"{runtime_tmp.project_dir}/meta").mkdir()
    pathlib.Path(f"{runtime_tmp.project_dir}/meta/main.yml").write_text(
        galaxy_info,
        encoding="utf-8",
    )

    runtime_tmp._install_galaxy_role(runtime_tmp.project_dir, role_name_check=1)
    assert "Computed fully qualified role name of " in caplog.text


def test_install_galaxy_role_no_checks(runtime_tmp: Runtime) -> None:
    """Check install role with bad namespace in galaxy info."""
    runtime_tmp.prepare_environment()
    pathlib.Path(f"{runtime_tmp.project_dir}/meta").mkdir()
    pathlib.Path(f"{runtime_tmp.project_dir}/meta/main.yml").write_text(
        """galaxy_info:
  role_name: foo
  author: bar
  namespace: acme
""",
    )
    runtime_tmp._install_galaxy_role(runtime_tmp.project_dir, role_name_check=2)
    result = runtime_tmp.run(["ansible-galaxy", "list"])
    assert "- acme.foo," in result.stdout
    assert result.returncode == 0, result


def test_upgrade_collection(runtime_tmp: Runtime) -> None:
    """Check that collection upgrade is possible."""
    # ensure that we inject our tmp folders in ansible paths
    runtime_tmp.prepare_environment()

    # we install specific oudated version of a collection
    runtime_tmp.install_collection("examples/reqs_v2/community-molecule-0.1.0.tar.gz")
    with pytest.raises(
        InvalidPrerequisiteError,
        match="Found community.molecule collection 0.1.0 but 9.9.9 or newer is required.",
    ):
        # we check that when install=False, we raise error
        runtime_tmp.require_collection("community.molecule", "9.9.9", install=False)
    # this should not fail, as we have this version
    runtime_tmp.require_collection("community.molecule", "0.1.0")


def test_require_collection_no_cache_dir() -> None:
    """Check require_collection without a cache directory."""
    runtime = Runtime()
    assert not runtime.cache_dir
    runtime.require_collection("community.molecule", "0.1.0", install=True)


def test_runtime_env_ansible_library(monkeypatch: MonkeyPatch) -> None:
    """Verify that custom path specified using ANSIBLE_LIBRARY is not lost."""
    path_name = "foo"
    monkeypatch.setenv("ANSIBLE_LIBRARY", path_name)

    path_name = os.path.realpath(path_name)
    runtime = Runtime()
    runtime.prepare_environment()
    assert path_name in runtime.config.default_module_path


@pytest.mark.parametrize(
    ("lower", "upper", "expected"),
    (
        ("1.0", "9999.0", True),
        (None, "9999.0", True),
        ("1.0", None, True),
        ("9999.0", None, False),
        (None, "1.0", False),
    ),
    ids=("1", "2", "3", "4", "5"),
)
def test_runtime_version_in_range(
    lower: str | None,
    upper: str | None,
    expected: bool,
) -> None:
    """Validate functioning of version_in_range."""
    runtime = Runtime()
    assert runtime.version_in_range(lower=lower, upper=upper) is expected


@pytest.mark.parametrize(
    ("path", "scenario", "expected_collections"),
    (
        pytest.param(
            "test/collections/acme.goodies",
            "default",
            [
                "ansible.posix",  # from tests/requirements.yml
                "ansible.utils",  # from galaxy.yml
                "community.molecule",  # from galaxy.yml
                "community.crypto",  # from galaxy.yml as a git dependency
            ],
            id="normal",
        ),
        pytest.param(
            "test/collections/acme.goodies/roles/baz",
            "deep_scenario",
            ["community.molecule"],
            id="deep",
        ),
    ),
)
def test_install_collection_from_disk(
    path: str,
    scenario: str,
    expected_collections: list[str],
) -> None:
    """Tests ability to install a local collection."""
    # ensure we do not have acme.goodies installed in user directory as it may
    # produce false positives
    rmtree(
        pathlib.Path(
            "~/.ansible/collections/ansible_collections/acme/goodies",
        ).expanduser(),
        ignore_errors=True,
    )
    with cwd(Path(path)):
        runtime = Runtime(isolated=True)
        # this should call install_collection_from_disk(".")
        runtime.prepare_environment(install_local=True)
        # that molecule converge playbook can be used without molecule and
        # should validate that the installed collection is available.
        result = runtime.run(["ansible-playbook", f"molecule/{scenario}/converge.yml"])
        assert result.returncode == 0, result.stdout
        runtime.load_collections()
        for collection_name in expected_collections:
            assert (
                collection_name in runtime.collections
            ), f"{collection_name} not found in {runtime.collections.keys()}"
        runtime.clean()


def test_install_collection_from_disk_fail() -> None:
    """Tests that we fail to install a broken collection."""
    with cwd(Path("test/collections/acme.broken")):
        runtime = Runtime(isolated=True)
        with pytest.raises(RuntimeError) as exc_info:
            runtime.prepare_environment(install_local=True)
        # based on version of Ansible used, we might get a different error,
        # but both errors should be considered acceptable
        assert exc_info.type in (
            RuntimeError,
            AnsibleCompatError,
            AnsibleCommandError,
            InvalidPrerequisiteError,
        )
        assert exc_info.match(
            "(is missing the following mandatory|Got 1 exit code while running: ansible-galaxy collection build)",
        )


def test_prepare_environment_offline_role() -> None:
    """Ensure that we can make use of offline roles."""
    with cwd(Path("test/roles/acme.missing_deps")):
        runtime = Runtime(isolated=True)
        runtime.prepare_environment(install_local=True, offline=True)


def test_runtime_run(runtime: Runtime) -> None:
    """Check if tee and non tee mode return same kind of results."""
    result1 = runtime.run(["seq", "10"])
    result2 = runtime.run(["seq", "10"], tee=True)
    assert result1.returncode == result2.returncode
    assert result1.stderr == result2.stderr
    assert result1.stdout == result2.stdout


def test_runtime_exec_cwd(runtime: Runtime) -> None:
    """Check if passing cwd works as expected."""
    path = Path("/")
    result1 = runtime.run(["pwd"], cwd=path)
    result2 = runtime.run(["pwd"])
    assert result1.stdout.rstrip() == str(path)
    assert result1.stdout != result2.stdout


def test_runtime_exec_env(runtime: Runtime) -> None:
    """Check if passing env works."""
    result = runtime.run(["printenv", "FOO"])
    assert not result.stdout

    result = runtime.run(["printenv", "FOO"], env={"FOO": "bar"})
    assert result.stdout.rstrip() == "bar"

    runtime.environ["FOO"] = "bar"
    result = runtime.run(["printenv", "FOO"])
    assert result.stdout.rstrip() == "bar"


def test_runtime_plugins(runtime: Runtime) -> None:
    """Tests ability to access detected plugins."""
    assert len(runtime.plugins.cliconf) == 0
    # ansible.netcommon.restconf might be in httpapi
    assert isinstance(runtime.plugins.httpapi, dict)
    # "ansible.netcommon.default" might be in runtime.plugins.netconf
    assert isinstance(runtime.plugins.netconf, dict)
    assert isinstance(runtime.plugins.role, dict)
    assert "become" in runtime.plugins.keyword

    if ansible_version() < Version("2.14.0"):
        assert "sudo" in runtime.plugins.become
        assert "memory" in runtime.plugins.cache
        assert "default" in runtime.plugins.callback
        assert "local" in runtime.plugins.connection
        assert "ini" in runtime.plugins.inventory
        assert "env" in runtime.plugins.lookup
        assert "sh" in runtime.plugins.shell
        assert "host_group_vars" in runtime.plugins.vars
        assert "file" in runtime.plugins.module
        assert "free" in runtime.plugins.strategy
        # ansible-doc below 2.14 does not support listing 'test' and 'filter' types:
        with pytest.raises(RuntimeError):
            assert "is_abs" in runtime.plugins.test
        with pytest.raises(RuntimeError):
            assert "bool" in runtime.plugins.filter
    else:
        assert "ansible.builtin.sudo" in runtime.plugins.become
        assert "ansible.builtin.memory" in runtime.plugins.cache
        assert "ansible.builtin.default" in runtime.plugins.callback
        assert "ansible.builtin.local" in runtime.plugins.connection
        assert "ansible.builtin.ini" in runtime.plugins.inventory
        assert "ansible.builtin.env" in runtime.plugins.lookup
        assert "ansible.builtin.sh" in runtime.plugins.shell
        assert "ansible.builtin.host_group_vars" in runtime.plugins.vars
        assert "ansible.builtin.file" in runtime.plugins.module
        assert "ansible.builtin.free" in runtime.plugins.strategy
        assert "ansible.builtin.is_abs" in runtime.plugins.test
        assert "ansible.builtin.bool" in runtime.plugins.filter


@pytest.mark.parametrize(
    ("path", "result"),
    (
        pytest.param(
            "test/assets/galaxy_paths",
            ["test/assets/galaxy_paths/foo/galaxy.yml"],
            id="1",
        ),
        pytest.param(
            "test/collections",
            [],  # should find nothing because these folders are not valid namespaces
            id="2",
        ),
        pytest.param(
            "test/assets/galaxy_paths/foo",
            ["test/assets/galaxy_paths/foo/galaxy.yml"],
            id="3",
        ),
    ),
)
def test_galaxy_path(path: str, result: list[str]) -> None:
    """Check behavior of galaxy path search."""
    assert search_galaxy_paths(Path(path)) == result


@pytest.mark.parametrize(
    ("name", "result"),
    (
        pytest.param(
            "foo",
            False,
            id="0",
        ),
        pytest.param(
            "git+git",
            True,
            id="1",
        ),
        pytest.param(
            "git@acme.com",
            True,
            id="2",
        ),
    ),
)
def test_is_url(name: str, result: bool) -> None:
    """Checks functionality of is_url."""
    assert is_url(name) == result


def test_prepare_environment_repair_broken_symlink(
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Ensure we can deal with broken symlinks in collections."""
    caplog.set_level(logging.INFO)
    project_dir = Path(__file__).parent / "collections" / "acme.minimal"
    runtime = Runtime(isolated=True, project_dir=project_dir)
    assert runtime.cache_dir
    acme = runtime.cache_dir / "collections" / "ansible_collections" / "acme"
    acme.mkdir(parents=True, exist_ok=True)
    goodies = acme / "minimal"
    rmtree(goodies, ignore_errors=True)
    goodies.unlink(missing_ok=True)
    goodies.symlink_to("/invalid/destination")
    runtime.prepare_environment(install_local=True)
    assert any(
        msg.startswith("Collection is symlinked, but not pointing to")
        for msg in caplog.messages
    )


def test_get_galaxy_role_name_invalid() -> None:
    """Verifies that function returns empty string on invalid input."""
    galaxy_infos = {
        "role_name": False,  # <-- invalid data, should be string
    }
    assert _get_galaxy_role_name(galaxy_infos) == ""


def test_runtime_has_playbook() -> None:
    """Tests has_playbook method."""
    runtime = Runtime(require_module=True)

    assert not runtime.has_playbook("this-does-not-exist.yml")
    # call twice to ensure cache is used:
    assert not runtime.has_playbook("this-does-not-exist.yml")

    assert not runtime.has_playbook("this-does-not-exist.yml", basedir=Path())
    # this is part of community.molecule collection
    assert runtime.has_playbook("community.molecule.validate.yml")
