#!/usr/bin/env bash

assert "mise tasks add xxx -- echo 'running xxx'"
assert "mise generate task-stubs"
assert "./bin/xxx" "running xxx"

# Windows cannot execute the `#!/bin/sh` stub, so a launcher is written beside it. Generated here
# too, not only on Windows: stubs are committed, and the contributor who runs one on Windows is not
# the person who generated it.
assert_succeed "test -f bin/xxx.cmd"
# Both words are quoted, and for the same reason: each is interpolated into a cmd line, where a
# space splits it and `&` would end the command outright. Quoting is unconditional -- a quoted bare
# name still resolves through PATH, so there is no case where it costs anything.
assert_contains "cat bin/xxx.cmd" '"mise" run "xxx" __MISE_LAUNCHER_ARGS__ %*'
# What marks the file as ours. Regeneration and cleanup both key off this rather than the shape of
# the command line, so that a hand-written batch file is never mistaken for a generated one.
assert_contains "cat bin/xxx.cmd" "rem generated by mise"

# The launcher recovers the arguments cmd.exe destroyed on the way in. `%*` alone cannot carry an
# argument containing `& ^ | " < >` or `%VAR%`, because cmd parses the line before the batch file
# runs; the original text survives in `CMDCMDLINE`, which the launcher copies out. Asserted here
# too, not only on Windows: this file is committed, so it is written on every host.
assert_contains "cat bin/xxx.cmd" "!CMDCMDLINE!"
# Delayed expansion, specifically: `%CMDCMDLINE%` is substituted before special characters are
# parsed and truncates the line at the first `&`, which is the character this exists to survive.
assert_fail "grep -q '%CMDCMDLINE%' bin/xxx.cmd"
# The guard, without which the `exit` below would close an interactive cmd session, and the `exit`,
# without which cmd would run whatever it queued after an unquoted `&` and report that failure as
# the task's exit code.
assert_contains "cat bin/xxx.cmd" "goto mise_launcher_fallback"
assert_contains "cat bin/xxx.cmd" "exit !ERRORLEVEL!"

# A launcher written before argument recovery existed is still recognised as ours. These files are
# committed, so refusing them would make regeneration fail on exactly the launchers that need
# replacing.
mkdir -p legacy-launcher-bin
printf '%s' '#!/bin/sh
# generated by mise task-stubs
exec mise run xxx "$@"' >legacy-launcher-bin/xxx
printf '@echo off\r\nrem generated by mise\r\n"mise" run "xxx" %%*\r\n' >legacy-launcher-bin/xxx.cmd
assert "mise generate task-stubs --dir legacy-launcher-bin"
# Replaced, not refused: the whole point of still recognising the old spelling.
assert_contains "cat legacy-launcher-bin/xxx.cmd" "!CMDCMDLINE!"

# Two files are written, so two are named. Counted exactly: `assert_contains` cannot tell one
# naming from two, and a run that reports one path while leaving a second behind is how the extra
# file goes unnoticed into a commit. Each path is then pinned on its own -- a total of two proves
# nothing about which two. Anchored, and the dot escaped, so `bin/xxx` cannot satisfy the launcher
# line and `xxxXcmd` cannot satisfy either.
assert "mise generate task-stubs 2>&1 | grep -c 'Wrote to'" "2"
assert "mise generate task-stubs 2>&1 | grep -c 'Wrote to bin/xxx$'" "1"
assert "mise generate task-stubs 2>&1 | grep -c 'Wrote to bin/xxx\.cmd$'" "1"

mkdir -p .mise/tasks/work/containers
cat >.mise/tasks/work/containers/_default <<'EOF'
#!/bin/sh
echo parent
EOF
cat >.mise/tasks/work/containers/pg <<'EOF'
#!/bin/sh
echo pg
EOF
cat >.mise/tasks/work/containers/redis <<'EOF'
#!/bin/sh
echo redis
EOF
chmod +x .mise/tasks/work/containers/{_default,pg,redis}

assert "mise generate task-stubs"
test -x bin/work/containers/_default
test -x bin/work/containers/pg
test -x bin/work/containers/redis
assert "bin/work/containers/_default" "parent"
assert "bin/work/containers/pg" "pg"
assert "bin/work/containers/redis" "redis"

# Launchers land inside nested stub directories as well.
assert_succeed "test -f bin/work/containers/pg.cmd"
assert_succeed "test -f bin/work/containers/_default.cmd"

# Regenerating walks that directory and refuses anything it did not write, so the launchers have to
# be recognised as generated -- without that, this second run fails.
assert "mise generate task-stubs"
assert "bin/work/containers/pg" "pg"

cat >.mise/tasks/legacy <<'EOF'
#!/bin/sh
echo legacy
EOF
chmod +x .mise/tasks/legacy
assert "mise generate task-stubs --dir legacy-bin"
test -f legacy-bin/legacy

# Preserve compatibility with stubs generated before the ownership marker.
# Generated stubs do not end with a newline, so reproduce the legacy output exactly.
printf '%s' '#!/bin/sh
exec mise run legacy "$@"' >legacy-bin/legacy

rm .mise/tasks/legacy
mkdir .mise/tasks/legacy
cat >.mise/tasks/legacy/_default <<'EOF'
#!/bin/sh
echo legacy
EOF
cat >.mise/tasks/legacy/child <<'EOF'
#!/bin/sh
echo child
EOF
chmod +x .mise/tasks/legacy/{_default,child}
assert "mise generate task-stubs --dir legacy-bin"
test -d legacy-bin/legacy
test -x legacy-bin/legacy/_default
test -x legacy-bin/legacy/child
assert "legacy-bin/legacy/_default" "legacy"
assert "legacy-bin/legacy/child" "child"

# The launcher goes with the stub it belonged to. Left behind, `legacy-bin/legacy.cmd` would keep
# running a task whose stub now lives at `legacy-bin/legacy/_default`.
test ! -e legacy-bin/legacy.cmd
test -f legacy-bin/legacy/_default.cmd

# Removing the nested task collapses the generated directory back to a leaf stub.
cat >legacy-bin/legacy/user-script <<'EOF'
#!/bin/sh
exec custom-tool run user-task "$@"
EOF
mv .mise/tasks/legacy/_default .mise/tasks/legacy-task
rm .mise/tasks/legacy/child
rmdir .mise/tasks/legacy
mv .mise/tasks/legacy-task .mise/tasks/legacy
assert_fail_contains \
  "mise generate task-stubs --dir legacy-bin" \
  "legacy-bin/legacy/user-script is not a generated task stub"
test -f legacy-bin/legacy/user-script
rm legacy-bin/legacy/user-script
assert "mise generate task-stubs --dir legacy-bin"
test -f legacy-bin/legacy
test ! -d legacy-bin/legacy
assert "legacy-bin/legacy" "legacy"

mkdir -p blocked-bin
echo "user-owned" >blocked-bin/work
assert_fail_contains \
  "mise generate task-stubs --dir blocked-bin" \
  "blocked-bin/work is not a directory"
test ! -e blocked-bin/xxx
assert "cat blocked-bin/work" "user-owned"

mkdir -p symlink-bin
echo "user-owned" >symlink-target
ln -s ../symlink-target symlink-bin/xxx
assert_fail_contains \
  "mise generate task-stubs --dir symlink-bin" \
  "symlink-bin/xxx is a symbolic link"
assert "cat symlink-target" "user-owned"
test -L symlink-bin/xxx
test ! -e symlink-bin/work

# `bin/<task>.cmd` is a plausible name for a script a project already keeps, and nothing about the
# name says mise owns it. Generation stops rather than overwriting one, and stops during validation,
# so no stub is written either.
mkdir -p owned-bin
printf '@echo off\r\necho mine\r\n' >owned-bin/xxx.cmd
assert_fail_contains \
  "mise generate task-stubs --dir owned-bin" \
  "owned-bin/xxx.cmd is not a generated launcher"
assert_contains "cat owned-bin/xxx.cmd" "echo mine"
test ! -e owned-bin/xxx

mkdir -p relocated-blocked-bin/work/containers
cat >relocated-blocked-bin/work/containers/_default <<'EOF'
#!/bin/sh
exec custom-tool run work:containers "$@"
EOF
assert_fail_contains \
  "mise generate task-stubs --dir relocated-blocked-bin" \
  "relocated-blocked-bin/work/containers/_default is not a generated task stub"
assert_contains "cat relocated-blocked-bin/work/containers/_default" "exec custom-tool run work:containers"
test ! -e relocated-blocked-bin/xxx

# A file task is named after its file, so the stub used to be too: `build.sh` produced
# `bin/build.sh`, and on Windows `build.bat` produced a `#!/bin/sh` script called `bin/build.bat`,
# which cmd.exe runs as a batch file line by line. The stub is named after the task now.
#
# Nothing is cleared first: the paths below (`bin/deploy*`, `bin/build*`) do not collide with what
# the scenarios above leave in `bin/` (`xxx*`, `work/containers/*`), so a reset would only be
# tidiness.
mkdir -p mise-tasks
cat >mise-tasks/deploy.sh <<'SH'
#!/usr/bin/env sh
echo deploying
SH
chmod +x mise-tasks/deploy.sh
cat >mise.toml <<'EOF2'
[tools]
EOF2

assert "mise generate task-stubs"
assert_succeed "test -f bin/deploy"
assert_succeed "test -f bin/deploy.cmd"
# The negative control: without it the assertion above would hold even if both spellings were
# written, which is the state this replaces.
assert_fail "test -f bin/deploy.sh"
assert "./bin/deploy" "deploying"

# A stub written under the old spelling is migrated, launcher and all, rather than left beside the
# new one still running the task.
cp bin/deploy bin/deploy.sh
cp bin/deploy.cmd bin/deploy.sh.cmd
chmod +x bin/deploy.sh
assert "mise generate task-stubs"
assert_fail "test -e bin/deploy.sh"
assert_fail "test -e bin/deploy.sh.cmd"
assert_succeed "test -f bin/deploy"

# ...but only a stub mise wrote. Anything else at that path stops the run, the same protection the
# nested-stub migration already has.
printf '#!/bin/sh\necho mine\n' >bin/deploy.sh
chmod +x bin/deploy.sh
assert_fail_contains "mise generate task-stubs" "is not the generated stub for task"
assert_contains "cat bin/deploy.sh" "echo mine"
# Not cleanup: this un-plants the file the two lines above deliberately planted. Left in place it
# keeps failing validation, and the next scenario asserts a successful run.
rm -f bin/deploy.sh

# Two file tasks differing only by extension share a display name, and on this platform both
# survive as separate tasks. They keep their file-named paths: renaming both onto `bin/build` would
# take a project that generates two stubs today and fail the run instead.
cat >mise-tasks/build.sh <<'SH'
#!/usr/bin/env sh
echo from-sh
SH
cat >mise-tasks/build.bat <<'SH'
#!/usr/bin/env sh
echo from-bat
SH
chmod +x mise-tasks/build.sh mise-tasks/build.bat
assert "mise generate task-stubs"
assert_succeed "test -f bin/build.sh"
assert_succeed "test -f bin/build.bat"
assert_fail "test -e bin/build"

# `--windows-launcher exe` copies the mise-shim.exe that ships with the Windows build, so there is
# nothing to copy here. It fails rather than falling back to the `.cmd`: stubs are committed, and
# quietly writing a different file from the one asked for puts it in someone's commit.
assert_fail_contains \
  "mise generate task-stubs --dir native-bin --windows-launcher exe" \
  "mise-shim.exe was not found"
# `deploy`, not `xxx`: `mise.toml` was overwritten above, so the task `mise tasks add` created at
# the top of this file no longer exists here and an assertion naming it would hold whatever the
# command did.
assert_fail "test -e native-bin/deploy"
assert_fail "test -e native-bin/deploy.exe"

# The default is untouched by the new flag, named explicitly so a change of default would fail
# here rather than in whichever scenario happened to notice.
assert "mise generate task-stubs --dir default-bin --windows-launcher cmd"
assert_succeed "test -f default-bin/deploy"
assert_succeed "test -f default-bin/deploy.cmd"
assert_fail "test -e default-bin/deploy.exe"

# `--mise-bin` pointing at a path only works on Windows if something Windows can execute is beside
# it. `mise generate install-script --write bin/mise` writes a `#!/usr/bin/env bash` script, and its
# own `.cmd` is opt-in (`--windows`, deliberately: it is a second committed file). Pairing the two
# as documented therefore leaves a `bin/<task>.cmd` that fails with `'"./bin/mise"' is not
# recognized` — so say so here, where it can still be fixed.
#
# Asserted on this host, not only on Windows, for the same reason the launcher is written on every
# host: whoever generates `bin/` on Linux is exactly the person who will not see the failure.
mkdir -p warn-bin
printf '#!/usr/bin/env bash\necho fake mise\n' >warn-bin/mise
chmod +x warn-bin/mise
assert_contains \
  "mise generate task-stubs --dir warn-stubs --mise-bin ./warn-bin/mise 2>&1" \
  "no Windows launcher beside it"
# The advice has to name the command that fixes it, not just the symptom.
assert_contains \
  "mise generate task-stubs --dir warn-stubs --mise-bin ./warn-bin/mise 2>&1" \
  "--windows"
# It is a warning, not a failure: the stubs are still what the user asked for, and on a POSIX host
# they work as they always did.
assert "mise generate task-stubs --dir warn-stubs --mise-bin ./warn-bin/mise"
# `deploy`, not `xxx`: `mise.toml` was overwritten above, so the task added at the top of this file
# no longer exists here and an assertion naming it would hold whatever the command did.
assert_succeed "test -f warn-stubs/deploy"

# With the launcher beside it there is nothing to say. This is the control: without it the
# assertions above would hold for a warning that fires unconditionally.
printf '@echo off\r\n' >warn-bin/mise.cmd
assert_fail "mise generate task-stubs --dir warn-stubs2 --mise-bin ./warn-bin/mise 2>&1 | grep -q 'no Windows launcher beside it'"

# The default resolves off PATH, where PATHEXT finds an executable for a bare name, so it must stay
# silent too -- otherwise the warning would fire on nearly every run of this command.
assert_fail "mise generate task-stubs --dir warn-stubs3 2>&1 | grep -q 'no Windows launcher beside it'"
