import json
import os
import signal
import subprocess
import sys
import textwrap
from pathlib import Path
from tempfile import TemporaryDirectory

import pytest

from pdm import termui
from pdm.cli import actions
from pdm.cli.utils import get_pep582_path
from pdm.utils import cd


@pytest.fixture
def _args(project):
    (project.root / "args.py").write_text(
        textwrap.dedent(
            """
            import os
            import sys
            name = sys.argv[1]
            args = ", ".join(sys.argv[2:])
            print(f"{name} CALLED with {args}" if args else f"{name} CALLED")
            """
        )
    )


def test_pep582_launcher_for_python_interpreter(project, local_finder, pdm):
    project.root.joinpath("main.py").write_text("import first;print(first.first([0, False, 1, 2]))\n")
    result = pdm(["add", "first"], obj=project)
    assert result.exit_code == 0, result.stderr
    env = os.environ.copy()
    env.update({"PYTHONPATH": get_pep582_path(project)})
    output = subprocess.check_output(
        [str(project.python.executable), str(project.root.joinpath("main.py"))],
        env=env,
    )
    assert output.decode().strip() == "1"


def test_auto_isolate_site_packages(project, pdm):
    env = os.environ.copy()
    env.update({"PYTHONPATH": get_pep582_path(project)})
    proc = subprocess.run(
        [str(project.python.executable), "-c", "import sys;print(sys.path, sep='\\n')"],
        env=env,
        capture_output=True,
        text=True,
        cwd=str(project.root),
        check=True,
    )
    assert any("site-packages" in path for path in proc.stdout.splitlines())

    result = pdm(
        ["run", "python", "-c", "import sys;print(sys.path, sep='\\n')"],
        obj=project,
        strict=True,
    )
    assert not any("site-packages" in path for path in result.stdout.splitlines())


def test_run_does_not_forward_posix_sigint(project, monkeypatch):
    from pdm.cli.commands.run import TaskRunner
    from pdm.cli.hooks import HookManager

    class DummyProcess:
        def wait(self):
            return 0

        def send_signal(self, signum):  # pragma: no cover
            raise AssertionError("SIGINT should not be forwarded on POSIX")

    signal_calls = []

    def fake_signal(signum, handler):
        signal_calls.append((signum, handler))
        return signal.SIG_DFL

    monkeypatch.setattr(sys, "platform", "linux")
    monkeypatch.setattr(subprocess, "Popen", lambda *args, **kwargs: DummyProcess())
    monkeypatch.setattr(signal, "signal", fake_signal)

    runner = TaskRunner(project, HookManager(project, []))
    assert runner._run_process(["python", "-c", "pass"], chdir=True) == 0

    assert (signal.SIGINT, signal.SIG_IGN) in signal_calls


def test_run_with_site_packages(project, pdm):
    project.pyproject.settings["scripts"] = {
        "foo": {
            "cmd": ["python", "-c", "import sys;print(sys.path, sep='\\n')"],
            "site_packages": True,
        }
    }
    project.pyproject.write()
    result = pdm(
        [
            "run",
            "--site-packages",
            "python",
            "-c",
            "import sys;print(sys.path, sep='\\n')",
        ],
        obj=project,
    )
    assert result.exit_code == 0
    result = pdm(["run", "foo"], obj=project)
    assert result.exit_code == 0


def test_run_command_not_found(pdm):
    result = pdm(["run", "foobar"])
    assert "Command 'foobar' is not found in your PATH." in result.stderr
    assert result.exit_code == 1


def test_run_pass_exit_code(pdm):
    result = pdm(["run", "python", "-c", "1/0"])
    assert result.exit_code == 1


def test_run_cmd_script(project, pdm):
    project.pyproject.settings["scripts"] = {"test_script": "python -V"}
    project.pyproject.write()
    result = pdm(["run", "test_script"], obj=project)
    assert result.exit_code == 0


def test_run_cmd_script_with_array(project, pdm):
    project.pyproject.settings["scripts"] = {"test_script": ["python", "-c", "import sys; sys.exit(22)"]}
    project.pyproject.write()
    result = pdm(["run", "test_script"], obj=project)
    assert result.exit_code == 22


def test_run_script_pass_project_root(project, pdm, capfd):
    project.pyproject.settings["scripts"] = {
        "test_script": [
            "python",
            "-c",
            "import os;print(os.getenv('PDM_PROJECT_ROOT'))",
        ]
    }
    project.pyproject.write()
    capfd.readouterr()
    result = pdm(["run", "test_script"], obj=project)
    assert result.exit_code == 0
    out, _ = capfd.readouterr()
    assert Path(out.strip()) == project.root


def test_run_shell_script(project, pdm):
    project.pyproject.settings["scripts"] = {
        "test_script": {
            "shell": "echo hello > output.txt",
            "help": "test it won't fail",
        }
    }
    project.pyproject.write()
    with cd(project.root):
        result = pdm(["run", "test_script"], obj=project)
    assert result.exit_code == 0
    assert (project.root / "output.txt").read_text().strip() == "hello"


def test_run_script_with_relative_path(project, pdm, capfd):
    if os.name == "nt":
        (project.root / "test_script.bat").write_text("@echo Hello\n")
    else:
        (project.root / "test_script.sh").write_text("#!/bin/bash\necho Hello\n")
        (project.root / "test_script.sh").chmod(0o755)
    with cd(project.root):
        pdm(["run", "./test_script.bat" if os.name == "nt" else "./test_script.sh"], obj=project, strict=True)
    out, _ = capfd.readouterr()
    assert out.strip() == "Hello"


def test_run_non_existing_local_script(project, pdm):
    with cd(project.root):
        result = pdm(["run", "./test_script.sh"], obj=project)
    assert result.exit_code != 0
    assert "not a valid executable" in result.stderr


@pytest.mark.parametrize(
    "args,expected",
    (
        pytest.param(["hello"], "ok hello", id="with-args"),
        pytest.param([], "ok", id="without-args"),
    ),
)
def test_run_shell_script_with_args_placeholder(project, pdm, args, expected):
    project.pyproject.settings["scripts"] = {
        "test_script": {
            "shell": "echo ok {args} > output.txt",
            "help": "test it won't fail",
        }
    }
    project.pyproject.write()
    with cd(project.root):
        result = pdm(["run", "test_script", *args], obj=project)
    assert result.exit_code == 0
    assert (project.root / "output.txt").read_text().strip() == expected


@pytest.mark.parametrize(
    "args,expected",
    (
        pytest.param(["hello"], "hello", id="with-args"),
        pytest.param([], "default", id="with-default"),
    ),
)
def test_run_shell_script_with_args_placeholder_with_default(project, pdm, args, expected):
    project.pyproject.settings["scripts"] = {
        "test_script": {
            "shell": "echo {args:default} > output.txt",
            "help": "test it won't fail",
        }
    }
    project.pyproject.write()
    with cd(project.root):
        result = pdm(["run", "test_script", *args], obj=project)
    assert result.exit_code == 0
    assert (project.root / "output.txt").read_text().strip() == expected


def test_run_call_script(project, pdm):
    (project.root / "test_script.py").write_text(
        textwrap.dedent(
            """
            import argparse
            import sys

            def main(argv=None):
                parser = argparse.ArgumentParser()
                parser.add_argument("-c", "--code", type=int)
                args = parser.parse_args(argv)
                sys.exit(args.code)
            """
        )
    )
    project.pyproject.settings["scripts"] = {
        "test_script": {"call": "test_script:main"},
        "test_script_with_args": {"call": "test_script:main(['-c', '9'])"},
    }
    project.pyproject.write()
    with cd(project.root):
        result = pdm(["run", "test_script", "-c", "8"], obj=project)
        assert result.exit_code == 8

        result = pdm(["run", "test_script_with_args"], obj=project)
        assert result.exit_code == 9


def test_run_script_with_extra_args(project, pdm, capfd):
    (project.root / "test_script.py").write_text(
        textwrap.dedent(
            """
            import sys
            print(*sys.argv[1:], sep='\\n')
            """
        )
    )
    project.pyproject.settings["scripts"] = {"test_script": "python test_script.py"}
    project.pyproject.write()
    with cd(project.root):
        pdm(["run", "test_script", "-a", "-b", "-c"], obj=project)
    out, _ = capfd.readouterr()
    assert out.splitlines()[-3:] == ["-a", "-b", "-c"]


@pytest.mark.parametrize(
    "args,expected",
    (
        pytest.param(["-a", "-b", "-c"], ["-a", "-b", "-c", "-x"], id="with-args"),
        pytest.param([], ["-x"], id="without-args"),
    ),
)
@pytest.mark.parametrize(
    "script",
    (
        pytest.param("python test_script.py {args} -x", id="as-str"),
        pytest.param(["python", "test_script.py", "{args}", "-x"], id="as-list"),
    ),
)
def test_run_script_with_args_placeholder(project, pdm, capfd, script, args, expected):
    (project.root / "test_script.py").write_text(
        textwrap.dedent(
            """
            import sys
            print(*sys.argv[1:], sep='\\n')
            """
        )
    )
    project.pyproject.settings["scripts"] = {"test_script": script}
    project.pyproject.write()
    with cd(project.root):
        pdm(["run", "-v", "test_script", *args], obj=project)
    out, _ = capfd.readouterr()
    assert out.strip().splitlines()[1:] == expected


@pytest.mark.parametrize(
    "args,expected",
    (
        pytest.param(["-a", "-b", "-c"], ["-a", "-b", "-c", "-x"], id="with-args"),
        pytest.param([], ["--default", "--value", "-x"], id="default"),
    ),
)
@pytest.mark.parametrize(
    "script",
    (
        pytest.param("python test_script.py {args:--default --value} -x", id="as-str"),
        pytest.param(["python", "test_script.py", "{args:--default --value}", "-x"], id="as-list"),
    ),
)
def test_run_script_with_args_placeholder_with_default(project, pdm, capfd, script, args, expected):
    (project.root / "test_script.py").write_text(
        textwrap.dedent(
            """
            import sys
            print(*sys.argv[1:], sep='\\n')
            """
        )
    )
    project.pyproject.settings["scripts"] = {"test_script": script}
    project.pyproject.write()
    with cd(project.root):
        pdm(["run", "-v", "test_script", *args], obj=project)
    out, _ = capfd.readouterr()
    assert out.strip().splitlines()[1:] == expected


def test_run_shell_script_with_pdm_placeholder(project, pdm):
    project.pyproject.settings["scripts"] = {
        "test_script": {
            "shell": "{pdm} -V > output.txt",
            "help": "test it won't fail",
        }
    }
    project.pyproject.write()
    with cd(project.root):
        result = pdm(["run", "test_script"], obj=project)
    assert result.exit_code == 0
    assert (project.root / "output.txt").read_text().strip().startswith("PDM, version")


def test_run_expand_env_vars(project, pdm, capfd, monkeypatch):
    (project.root / "test_script.py").write_text("import os; print(os.getenv('FOO'))")
    project.pyproject.settings["scripts"] = {
        "test_cmd": 'python -c "foo, bar = 0, 1;print(${FOO})"',
        "test_cmd_no_expand": "python -c 'print(${FOO})'",
        "test_script": "python test_script.py",
        "test_cmd_array": ["python", "test_script.py"],
        "test_shell": {"shell": "echo %FOO%" if os.name == "nt" else "echo $FOO"},
    }
    project.pyproject.write()
    capfd.readouterr()
    with cd(project.root):
        monkeypatch.setenv("FOO", "bar")
        pdm(["run", "test_cmd"], obj=project)
        assert capfd.readouterr()[0].strip() == "1"

        result = pdm(["run", "test_cmd_no_expand"], obj=project)
        assert result.exit_code == 1

        pdm(["run", "test_script"], obj=project)
        assert capfd.readouterr()[0].strip() == "bar"

        pdm(["run", "test_cmd_array"], obj=project)
        assert capfd.readouterr()[0].strip() == "bar"

        pdm(["run", "test_shell"], obj=project)
        assert capfd.readouterr()[0].strip() == "bar"


def test_run_expand_env_vars_from_config(project, pdm, capfd):
    (project.root / "test_script.py").write_text("import os; print(os.getenv('FOO'))")
    project.pyproject.settings["scripts"] = {
        "test_cmd": 'python -c "foo, bar = 0, 1;print(${FOO})"',
        "test_cmd_no_expand": "python -c 'print(${FOO})'",
        "test_script": "python test_script.py",
        "test_cmd_array": ["python", "test_script.py"],
        "test_shell": {"shell": "echo %FOO%" if os.name == "nt" else "echo $FOO"},
        "_": {"env": {"FOO": "bar"}},
    }
    project.pyproject.write()
    capfd.readouterr()
    with cd(project.root):
        pdm(["run", "test_cmd"], obj=project)
        assert capfd.readouterr()[0].strip() == "1"

        result = pdm(["run", "test_cmd_no_expand"], obj=project)
        assert result.exit_code == 1

        pdm(["run", "test_script"], obj=project)
        assert capfd.readouterr()[0].strip() == "bar"

        pdm(["run", "test_cmd_array"], obj=project)
        assert capfd.readouterr()[0].strip() == "bar"

        pdm(["run", "test_shell"], obj=project)
        assert capfd.readouterr()[0].strip() == "bar"


def test_run_script_with_env_defined(project, pdm, capfd):
    (project.root / "test_script.py").write_text("import os; print(os.getenv('FOO'))")
    project.pyproject.settings["scripts"] = {"test_script": {"cmd": "python test_script.py", "env": {"FOO": "bar"}}}
    project.pyproject.write()
    capfd.readouterr()
    with cd(project.root):
        pdm(["run", "test_script"], obj=project)
        assert capfd.readouterr()[0].strip() == "bar"


def test_run_script_with_dotenv_file(project, pdm, capfd, monkeypatch):
    (project.root / "test_script.py").write_text("import os; print(os.getenv('FOO'), os.getenv('BAR'))")
    project.pyproject.settings["scripts"] = {
        "test_override": {
            "cmd": "python test_script.py",
            "env_file": {"override": ".env"},
        },
        "test_default": {"cmd": "python test_script.py", "env_file": ".env"},
    }
    project.pyproject.write()
    monkeypatch.setenv("BAR", "foo")
    (project.root / ".env").write_text("FOO=bar\nBAR=override")
    capfd.readouterr()
    with cd(project.root):
        pdm(["run", "test_default"], obj=project)
        assert capfd.readouterr()[0].strip() == "bar foo"
        pdm(["run", "test_override"], obj=project)
        assert capfd.readouterr()[0].strip() == "bar override"


def test_run_script_override_global_env(project, pdm, capfd):
    (project.root / "test_script.py").write_text("import os; print(os.getenv('FOO'))")
    project.pyproject.settings["scripts"] = {
        "_": {"env": {"FOO": "bar"}},
        "test_env": {"cmd": "python test_script.py"},
        "test_env_override": {"cmd": "python test_script.py", "env": {"FOO": "foobar"}},
    }
    project.pyproject.write()
    capfd.readouterr()
    with cd(project.root):
        pdm(["run", "test_env"], obj=project)
        assert capfd.readouterr()[0].strip() == "bar"
        pdm(["run", "test_env_override"], obj=project)
        assert capfd.readouterr()[0].strip() == "foobar"


def test_run_show_list_of_scripts(project, pdm):
    project.pyproject.settings["scripts"] = {
        "test_composite": {"composite": ["test_cmd", "test_script", "test_shell"]},
        "test_cmd": "flask db upgrade",
        "test_multi": """\
            I am a multilines
            command
        """,
        "test_script": {"call": "test_script:main", "help": "call a python function"},
        "test_shell": {"shell": "echo $FOO", "help": "shell command"},
    }
    project.pyproject.write()
    result = pdm(["run", "--list"], obj=project)
    result_lines = result.output.splitlines()[3:]
    assert result_lines[0][1:-1].strip() == "test_cmd       │ cmd       │ flask db upgrade"
    sep = termui.Emoji.ARROW_SEPARATOR
    assert result_lines[1][1:-1].strip() == f"test_composite │ composite │ test_cmd {sep} test_script {sep} test_shell"
    assert result_lines[2][1:-1].strip() == f"test_multi     │ cmd       │ I am a multilines{termui.Emoji.ELLIPSIS}"
    assert result_lines[3][1:-1].strip() == "test_script    │ call      │ call a python function"
    assert result_lines[4][1:-1].strip() == "test_shell     │ shell     │ shell command"


def test_run_show_list_of_scripts_hide_internals(project, pdm):
    project.pyproject.settings["scripts"] = {
        "public": "true",
        "_internal": "true",
    }
    project.pyproject.write()
    result = pdm(["run", "--list"], obj=project)
    assert "public" in result.output
    assert "_internal" not in result.output


def test_run_json_list_of_scripts(project, pdm):
    project.pyproject.settings["scripts"] = {
        "_": {"env_file": ".env"},
        "test_composite": {"composite": ["test_cmd", "test_script", "test_shell"]},
        "test_cmd": "flask db upgrade",
        "test_multi": """\
            I am a multilines
            command
        """,
        "test_script": {"call": "test_script:main", "help": "call a python function"},
        "test_shell": {"shell": "echo $FOO", "help": "shell command"},
        "test_env": {"cmd": "true", "env": {"TEST": "value"}},
        "test_env_file": {"cmd": "true", "env_file": ".env"},
        "test_override": {"cmd": "true", "env_file": {"override": ".env"}},
        "test_site_packages": {"cmd": "true", "site_packages": True},
        "_private": "true",
    }
    project.pyproject.write()
    result = pdm(["run", "--json"], obj=project, strict=True)

    sep = termui.Emoji.ARROW_SEPARATOR
    assert json.loads(result.outputs) == {
        "_": {"name": "_", "help": "Shared options", "kind": "shared", "env_file": ".env"},
        "test_cmd": {"name": "test_cmd", "help": "flask db upgrade", "kind": "cmd", "args": "flask db upgrade"},
        "test_composite": {
            "name": "test_composite",
            "help": f"test_cmd {sep} test_script {sep} test_shell",
            "kind": "composite",
            "args": ["test_cmd", "test_script", "test_shell"],
        },
        "test_multi": {
            "name": "test_multi",
            "help": f"I am a multilines{termui.Emoji.ELLIPSIS}",
            "kind": "cmd",
            "args": "            I am a multilines\n            command\n        ",
        },
        "test_script": {
            "name": "test_script",
            "help": "call a python function",
            "kind": "call",
            "args": "test_script:main",
        },
        "test_shell": {"name": "test_shell", "help": "shell command", "kind": "shell", "args": "echo $FOO"},
        "test_env": {"name": "test_env", "help": "true", "kind": "cmd", "args": "true", "env": {"TEST": "value"}},
        "test_env_file": {"name": "test_env_file", "help": "true", "kind": "cmd", "args": "true", "env_file": ".env"},
        "test_override": {
            "name": "test_override",
            "help": "true",
            "kind": "cmd",
            "args": "true",
            "env_file.override": ".env",
        },
        "test_site_packages": {
            "name": "test_site_packages",
            "help": "true",
            "kind": "cmd",
            "args": "true",
            "site_packages": True,
        },
        "_private": {
            "name": "_private",
            "help": "true",
            "kind": "cmd",
            "args": "true",
        },
    }


@pytest.mark.usefixtures("local_finder")
@pytest.mark.parametrize("explicit_python", [True, False])
def test_run_with_another_project_root(project, pdm, capfd, explicit_python):
    project.pyproject.metadata["requires-python"] = ">=3.6"
    project.pyproject.write()
    pdm(["add", "first"], obj=project)
    with TemporaryDirectory(prefix="pytest-run-") as tmp_dir:
        Path(tmp_dir).joinpath("main.py").write_text("import first;print(first.first([0, False, 1, 2]))\n")
        capfd.readouterr()
        with cd(tmp_dir):
            args = ["run", "-p", str(project.root), "main.py"]
            if explicit_python:
                args.insert(len(args) - 1, "python")
            ret = pdm(args)
            out, err = capfd.readouterr()
            assert ret.exit_code == 0, err
            assert out.strip() == "1"


def test_import_another_sitecustomize(project, pdm, capfd):
    project.pyproject.metadata["requires-python"] = ">=2.7"
    project.pyproject.write()
    # a script for checking another sitecustomize is imported
    project.root.joinpath("foo.py").write_text("import os;print(os.getenv('FOO'))")
    # ensure there have at least one sitecustomize can be imported
    # there may have more than one sitecustomize.py in sys.path
    project.root.joinpath("sitecustomize.py").write_text("import os;os.environ['FOO'] = 'foo'")
    env = os.environ.copy()
    paths = env.get("PYTHONPATH")
    this_path = str(project.root)
    new_paths = [this_path] if not paths else [this_path, paths]
    env["PYTHONPATH"] = os.pathsep.join(new_paths)
    project._environment = None
    capfd.readouterr()
    with cd(project.root):
        result = pdm(["run", "python", "foo.py"], env=env)
    assert result.exit_code == 0, result.stderr
    out, _ = capfd.readouterr()
    assert out.strip() == "foo"


def test_run_with_patched_sysconfig(project, pdm, capfd):
    project.root.joinpath("script.py").write_text(
        """\
import sysconfig
import json
print(json.dumps(sysconfig.get_paths()))
"""
    )
    capfd.readouterr()
    with cd(project.root):
        result = pdm(["run", "python", "script.py"], obj=project)
    assert result.exit_code == 0
    out = json.loads(capfd.readouterr()[0])
    assert "__pypackages__" in out["purelib"]


def test_run_composite(project, pdm, capfd, _echo):
    project.pyproject.settings["scripts"] = {
        "first": "python echo.py First",
        "second": "python echo.py Second",
        "test": {"composite": ["first", "second"]},
    }
    project.pyproject.write()
    capfd.readouterr()
    pdm(["run", "test"], strict=True, obj=project)
    out, _ = capfd.readouterr()
    assert "First CALLED" in out
    assert "Second CALLED" in out


def test_composite_stops_on_first_failure(project, pdm, capfd):
    project.pyproject.settings["scripts"] = {
        "first": {"cmd": ["python", "-c", "print('First CALLED')"]},
        "fail": "python -c 'raise Exception'",
        "second": "echo 'Second CALLED'",
        "test": {"composite": ["first", "fail", "second"]},
    }
    project.pyproject.write()
    capfd.readouterr()
    result = pdm(["run", "test"], obj=project)
    assert result.exit_code == 1
    out, _ = capfd.readouterr()
    assert "First CALLED" in out
    assert "Second CALLED" not in out


def test_composite_keep_going_on_failure(project, pdm, capfd):
    project.pyproject.settings["scripts"] = {
        "first": {"cmd": ["python", "-c", "print('First CALLED')"]},
        "fail": "python -c 'raise Exception'",
        "second": "echo 'Second CALLED'",
        "test": {"composite": ["first", "fail", "second"], "keep_going": True},
    }
    project.pyproject.write()
    capfd.readouterr()
    result = pdm(["run", "test"], obj=project)
    assert result.exit_code == 1
    out, _ = capfd.readouterr()
    assert "First CALLED" in out
    assert "Second CALLED" in out


def test_composite_inherit_env(project, pdm, capfd, _echo):
    project.pyproject.settings["scripts"] = {
        "first": {
            "cmd": "python echo.py First VAR",
            "env": {"VAR": "42"},
        },
        "second": {
            "cmd": "python echo.py Second VAR",
            "env": {"VAR": "42"},
        },
        "nested": {
            "composite": ["third"],
            "env": {"VAR": "42"},
        },
        "third": {
            "cmd": "python echo.py Third VAR",
            "env": {"VAR": "42"},
        },
        "test": {"composite": ["first", "second", "nested"], "env": {"VAR": "overridden"}},
    }
    project.pyproject.write()
    capfd.readouterr()
    pdm(["run", "test"], strict=True, obj=project)
    out, _ = capfd.readouterr()
    assert "First CALLED with VAR=overridden" in out
    assert "Second CALLED with VAR=overridden" in out
    assert "Third CALLED with VAR=overridden" in out


def test_composite_fail_on_first_missing_task(project, pdm, capfd, _echo):
    project.pyproject.settings["scripts"] = {
        "first": "python echo.py First",
        "second": "python echo.py Second",
        "test": {"composite": ["first", "fail", "second"]},
    }
    project.pyproject.write()
    capfd.readouterr()
    result = pdm(["run", "test"], obj=project)
    assert result.exit_code == 1
    out, _ = capfd.readouterr()
    assert "First CALLED" in out
    assert "Second CALLED" not in out


def test_composite_fails_on_recursive_script(project, pdm):
    project.pyproject.settings["scripts"] = {
        "first": {"composite": ["first"]},
        "second": {"composite": ["third"]},
        "third": {"composite": ["second"]},
        "fourth": {"composite": ["python -V", "python -V"]},
        "fifth": {"composite": ["fourth", "fourth"]},
    }
    project.pyproject.write()
    result = pdm(["run", "first"], obj=project)
    assert result.exit_code == 1
    assert "Script first is recursive" in result.stderr

    result = pdm(["run", "second"], obj=project)
    assert result.exit_code == 1
    assert "Script second is recursive" in result.stderr

    result = pdm(["run", "fourth"], obj=project)
    assert result.exit_code == 0

    result = pdm(["run", "fifth"], obj=project)
    assert result.exit_code == 0


def test_composite_runs_all_hooks(project, pdm, capfd, _echo):
    project.pyproject.settings["scripts"] = {
        "test": {"composite": ["first", "second"]},
        "pre_test": "python echo.py Pre-Test",
        "post_test": "python echo.py Post-Test",
        "first": "python echo.py First",
        "pre_first": "python echo.py Pre-First",
        "second": "python echo.py Second",
        "post_second": "python echo.py Post-Second",
    }
    project.pyproject.write()
    capfd.readouterr()
    pdm(["run", "test"], strict=True, obj=project)
    out, _ = capfd.readouterr()
    assert "Pre-Test CALLED" in out
    assert "Pre-First CALLED" in out
    assert "First CALLED" in out
    assert "Second CALLED" in out
    assert "Post-Second CALLED" in out
    assert "Post-Test CALLED" in out


def test_composite_pass_parameters_to_subtasks(project, pdm, capfd, _args):
    project.pyproject.settings["scripts"] = {
        "test": {"composite": ["first", "second"]},
        "pre_test": "python args.py Pre-Test",
        "post_test": "python args.py Post-Test",
        "first": "python args.py First",
        "pre_first": "python args.py Pre-First",
        "second": "python args.py Second",
        "post_second": "python args.py Post-Second",
    }
    project.pyproject.write()
    capfd.readouterr()
    pdm(["run", "test", "param=value"], strict=True, obj=project)
    out, _ = capfd.readouterr()
    assert "Pre-Test CALLED" in out
    assert "Pre-First CALLED" in out
    assert "First CALLED with param=value" in out
    assert "Second CALLED with param=value" in out
    assert "Post-Second CALLED" in out
    assert "Post-Test CALLED" in out


def test_composite_can_pass_parameters(project, pdm, capfd, _args):
    project.pyproject.settings["scripts"] = {
        "test": {"composite": ["first param=first", "second param=second"]},
        "pre_test": "python args.py Pre-Test",
        "post_test": "python args.py Post-Test",
        "first": "python args.py First",
        "pre_first": "python args.py Pre-First",
        "second": "python args.py Second",
        "post_second": "python args.py Post-Second",
    }
    project.pyproject.write()
    capfd.readouterr()
    pdm(["run", "test"], strict=True, obj=project)
    out, _ = capfd.readouterr()
    assert "Pre-Test CALLED" in out
    assert "Pre-First CALLED" in out
    assert "First CALLED with param=first" in out
    assert "Second CALLED with param=second" in out
    assert "Post-Second CALLED" in out
    assert "Post-Test CALLED" in out


@pytest.mark.parametrize(
    "args,expected",
    (
        pytest.param(["-a"], "-a, ", id="with-args"),
        pytest.param([], "", id="without-args"),
    ),
)
def test_composite_only_pass_parameters_to_subtasks_with_args(project, pdm, capfd, _args, args, expected):
    project.pyproject.settings["scripts"] = {
        "test": {"composite": ["first", "second {args} key=value"]},
        "first": "python args.py First",
        "second": "python args.py Second",
    }
    project.pyproject.write()
    capfd.readouterr()
    pdm(["run", "-v", "test", *args], strict=True, obj=project)
    out, _ = capfd.readouterr()
    assert "First CALLED" in out
    assert f"Second CALLED with {expected}key=value" in out


@pytest.mark.parametrize(
    "args,expected",
    (
        pytest.param(["-a"], "-a", id="with-args"),
        pytest.param([], "--default", id="default"),
    ),
)
def test_composite_only_pass_parameters_to_subtasks_with_args_with_default(project, pdm, capfd, _args, args, expected):
    project.pyproject.settings["scripts"] = {
        "test": {"composite": ["first", "second {args:--default} key=value"]},
        "first": "python args.py First",
        "second": "python args.py Second",
    }
    project.pyproject.write()
    capfd.readouterr()
    pdm(["run", "-v", "test", *args], strict=True, obj=project)
    out, _ = capfd.readouterr()
    assert "First CALLED" in out
    assert f"Second CALLED with {expected}, key=value" in out


def test_composite_hooks_inherit_env(project, pdm, capfd, _echo):
    project.pyproject.settings["scripts"] = {
        "pre_task": {"cmd": "python echo.py Pre-Task VAR", "env": {"VAR": "42"}},
        "task": "python echo.py Task",
        "post_task": {"cmd": "python echo.py Post-Task VAR", "env": {"VAR": "42"}},
        "test": {"composite": ["task"], "env": {"VAR": "overridden"}},
    }
    project.pyproject.write()
    capfd.readouterr()
    pdm(["run", "test"], strict=True, obj=project)
    out, _ = capfd.readouterr()
    assert "Pre-Task CALLED with VAR=overridden" in out
    assert "Task CALLED" in out
    assert "Post-Task CALLED with VAR=overridden" in out


def test_composite_inherit_env_in_cascade(project, pdm, capfd, _echo):
    project.pyproject.settings["scripts"] = {
        "_": {"env": {"FOO": "BAR", "TIK": "TOK"}},
        "pre_task": {
            "cmd": "python echo.py Pre-Task VAR FOO TIK",
            "env": {"VAR": "42", "FOO": "foobar"},
        },
        "task": {
            "cmd": "python echo.py Task VAR FOO TIK",
            "env": {"VAR": "42", "FOO": "foobar"},
        },
        "post_task": {
            "cmd": "python echo.py Post-Task VAR FOO TIK",
            "env": {"VAR": "42", "FOO": "foobar"},
        },
        "test": {"composite": ["task"], "env": {"VAR": "overridden"}},
    }
    project.pyproject.write()
    capfd.readouterr()
    pdm(["run", "test"], strict=True, obj=project)
    out, _ = capfd.readouterr()
    assert "Pre-Task CALLED with VAR=overridden FOO=foobar TIK=TOK" in out
    assert "Task CALLED with VAR=overridden FOO=foobar TIK=TOK" in out
    assert "Post-Task CALLED with VAR=overridden FOO=foobar TIK=TOK" in out


def test_composite_inherit_dotfile(project, pdm, capfd, _echo):
    (project.root / ".env").write_text("VAR=42")
    (project.root / "override.env").write_text("VAR=overridden")
    project.pyproject.settings["scripts"] = {
        "pre_task": {"cmd": "python echo.py Pre-Task VAR", "env_file": ".env"},
        "task": {"cmd": "python echo.py Task VAR", "env_file": ".env"},
        "post_task": {"cmd": "python echo.py Post-Task VAR", "env_file": ".env"},
        "test": {"composite": ["task"], "env_file": "override.env"},
    }
    project.pyproject.write()
    capfd.readouterr()
    pdm(["run", "test"], strict=True, obj=project)
    out, _ = capfd.readouterr()
    assert "Pre-Task CALLED with VAR=overridden" in out
    assert "Task CALLED with VAR=overridden" in out
    assert "Post-Task CALLED with VAR=overridden" in out


def test_resolve_env_vars_in_dotfile(project, pdm, capfd, _echo):
    (project.root / ".env").write_text("VAR=42\nFOO=${OUT}/${VAR}")
    project.pyproject.settings["scripts"] = {
        "_": {"env_file": ".env"},
        "test": {"cmd": "python echo.py Task FOO BAR", "env": {"BAR": "${FOO}/bar"}},
    }
    project.pyproject.write()
    capfd.readouterr()
    pdm(["run", "test"], strict=True, obj=project, env={"OUT": "hello"})
    out, _ = capfd.readouterr()
    assert "Task CALLED with FOO=hello/42 BAR=hello/42/bar" in out


def test_composite_can_have_commands(project, pdm, capfd):
    project.pyproject.settings["scripts"] = {
        "task": {"cmd": ["python", "-c", 'print("Task CALLED")']},
        "test": {"composite": ["task", "python -c 'print(\"Command CALLED\")'"]},
    }
    project.pyproject.write()
    capfd.readouterr()
    pdm(["run", "-v", "test"], strict=True, obj=project)
    out, _ = capfd.readouterr()
    assert "Task CALLED" in out
    assert "Command CALLED" in out


def test_run_shortcut(project, pdm, capfd):
    project.pyproject.settings["scripts"] = {
        "test": "echo 'Everything is fine'",
    }
    project.pyproject.write()
    capfd.readouterr()
    result = pdm(["test"], obj=project, strict=True)
    assert result.exit_code == 0
    out, _ = capfd.readouterr()
    assert "Everything is fine" in out


def test_run_shortcuts_dont_override_commands(project, pdm, capfd, mocker):
    do_lock = mocker.patch.object(actions, "do_lock")
    do_sync = mocker.patch.object(actions, "do_sync")
    project.pyproject.settings["scripts"] = {
        "install": "echo 'Should not run'",
    }
    project.pyproject.write()
    capfd.readouterr()
    result = pdm(["install"], obj=project, strict=True)
    assert result.exit_code == 0
    out, _ = capfd.readouterr()
    assert "Should not run" not in out
    do_lock.assert_called_once()
    do_sync.assert_called_once()


def test_run_shortcut_fail_with_usage_if_script_not_found(project, pdm):
    result = pdm(["whatever"], obj=project)
    assert result.exit_code != 0
    assert "Command not found: whatever" in result.stderr
    assert "usage" in result.stderr.lower()


@pytest.mark.parametrize(
    "args",
    [
        pytest.param(["-ko"], id="unknown param"),
        pytest.param(["pip", "--version"], id="not an user script"),
    ],
)
def test_empty_positional_args_still_display_usage(project, pdm, args):
    result = pdm(args, obj=project)
    assert result.exit_code != 0
    assert "usage" in result.stderr.lower()


def test_empty_positional_args_display_help(project, pdm):
    result = pdm([], obj=project)
    assert result.exit_code == 0
    assert "usage:" in result.output.lower()
    assert "commands:" in result.output.lower()
    assert "options:" in result.output.lower()


def test_run_script_changing_working_dir(project, pdm, capfd):
    project.root.joinpath("subdir").mkdir()
    project.root.joinpath("subdir", "file.text").write_text("Hello world\n")
    project.pyproject.settings["scripts"] = {
        "test_script": {"working_dir": "subdir", "cmd": "cat file.text"},
    }
    project.pyproject.write()
    capfd.readouterr()
    pdm(["run", "test_script"], obj=project, strict=True)
    assert capfd.readouterr()[0].strip() == "Hello world"


def test_run_script_with_inline_metadata(project, pdm, local_finder, local_finder_artifacts):
    with cd(project.root):
        project.root.joinpath("test_script.py").write_text(
            textwrap.dedent("""\
            from first import first

            assert first([0, False, 1, 2]) == 1
            """)
        )
        result = pdm(["run", "test_script.py"], obj=project)
        assert result.exit_code != 0

    local_artifacts_url = local_finder_artifacts.as_uri()

    project.root.joinpath("test_script.py").write_text(
        textwrap.dedent(f"""\
        # /// script
        # requires-python = ">=3.9"
        # dependencies = [
        #   "first",
        # ]
        #
        # [[tool.pdm.source]]
        # name = "pypi"
        # url = "{local_artifacts_url}"
        # type = "find_links"
        # ///
        from first import first

        assert first([0, False, 1, 2]) == 1
        """)
    )
    with cd(project.root):
        result = pdm(["run", "test_script.py"], obj=project)
        assert result.exit_code == 0


def test_run_script_pass_run_cwd(project, pdm, capfd):
    project.pyproject.settings["scripts"] = {
        "test_script": [
            "python",
            "-c",
            "import os;print(os.getenv('PDM_RUN_CWD'))",
        ]
    }
    project.pyproject.write()
    capfd.readouterr()
    result = pdm(["run", "test_script"], obj=project)
    assert result.exit_code == 0
    out, _ = capfd.readouterr()
    assert Path(out.strip()) == Path.cwd()


def test_run_script_pass_run_cwd_to_original_working_dir_when_working_dir_of_script_is_changed(project, pdm, capfd):
    project.root.joinpath("subdir").mkdir()
    project.root.joinpath("subdir", "test_script.py").write_text(
        textwrap.dedent("""\
           import os
           print(os.getenv('PDM_RUN_CWD', ''))
        """)
    )
    project.pyproject.settings["scripts"] = {
        "test_script": {"working_dir": "subdir", "cmd": "python test_script.py"},
    }
    project.pyproject.write()
    capfd.readouterr()
    pdm(["run", "test_script"], obj=project, strict=True)
    assert capfd.readouterr()[0].strip() == str(Path.cwd())


def test_run_script_default_verbosity(project, pdm):
    project.pyproject.settings["scripts"] = {"test": {"cmd": "python -V", "help": "help"}}
    project.pyproject.write()

    result = pdm(["run", "test"], strict=True, obj=project)

    assert "task test" not in result.stderr
    assert "['python', '-V']" not in result.stderr
    assert "help" not in result.stderr


def test_run_script_default_verbosity_with_show_header(project, pdm):
    project.project_config.update({"scripts.show_header": True})
    project.pyproject.settings["scripts"] = {"test": {"cmd": "python -V", "help": "help"}}
    project.pyproject.write()

    result = pdm(["run", "test"], strict=True, obj=project)

    assert "task test" in result.stderr
    assert "['python', '-V']" not in result.stderr
    assert "python -V" not in result.stderr
    assert "help" in result.stderr


def test_run_script_default_verbosity_with_show_header_no_help(project, pdm):
    project.project_config.update({"scripts.show_header": True})
    project.pyproject.settings["scripts"] = {"test": {"cmd": "python -V"}}
    project.pyproject.write()

    result = pdm(["run", "test"], strict=True, obj=project)

    assert "task test" in result.stderr
    assert "python -V" in result.stderr
    assert "['python', '-V']" not in result.stderr


def test_run_script_verbose(project, pdm):
    project.pyproject.settings["scripts"] = {"test": {"cmd": "python -V", "help": "help"}}
    project.pyproject.write()

    result = pdm(["run", "-v", "test"], strict=True, obj=project)

    assert "task test" in result.stderr
    assert "['python', '-V']" in result.stderr
    assert "python -V" not in result.stderr
    assert "help" not in result.stderr


def test_run_composite_script_default_verbosity_with_show_header(project, pdm):
    project.project_config.update({"scripts.show_header": True})
    project.pyproject.settings["scripts"] = {
        "test": {"cmd": "python -V", "help": "help"},
        "parent": {"composite": ["test", "test"]},
    }
    project.pyproject.write()

    result = pdm(["run", "parent"], strict=True, obj=project)

    assert "task parent" in result.stderr
    assert f"test {termui.Emoji.ARROW_SEPARATOR} test" in result.stderr
    assert "['test', 'test']" not in result.stderr
    assert "task test" in result.stderr
    assert "python -V" not in result.stderr
    assert "['python', '-V']" not in result.stderr
    assert "help" in result.stderr


def test_run_composite_script_default_verbosity_with_show_header_and_help(project, pdm):
    project.project_config.update({"scripts.show_header": True})
    project.pyproject.settings["scripts"] = {
        "test": {"cmd": "python -V", "help": "help"},
        "parent": {"composite": ["test", "test"], "help": "composite"},
    }
    project.pyproject.write()

    result = pdm(["run", "parent"], strict=True, obj=project)

    assert "task parent" in result.stderr
    assert "composite" in result.stderr
    assert f"test {termui.Emoji.ARROW_SEPARATOR} test" not in result.stderr
    assert "['test', 'test']" not in result.stderr
    assert "task test" in result.stderr
    assert "['python', '-V']" not in result.stderr
    assert "python -V" not in result.stderr
    assert "help" in result.stderr


def test_run_composite_script_verbose(project, pdm):
    project.pyproject.settings["scripts"] = {
        "test": {"cmd": "python -V", "help": "help"},
        "parent": {"composite": ["test", "test"], "help": "composite"},
    }
    project.pyproject.write()

    result = pdm(["run", "-v", "parent"], strict=True, obj=project)

    assert "task parent" in result.stderr
    assert "composite" not in result.stderr
    assert f"test {termui.Emoji.ARROW_SEPARATOR} test" not in result.stderr
    assert "['test', 'test']" in result.stderr
    assert "task test" in result.stderr
    assert "['python', '-V']" in result.stderr
    assert "python -V" not in result.stderr
    assert "help" not in result.stderr
