#!/usr/bin/env bash

# {{ secrets.X }} is allowed only in a task's own env values. It is rendered just before the
# task starts, the reference is the grant, and the value never reaches config or caches.

mkdir -p "$HOME/bin"
printf '#!/usr/bin/env bash\nexec python3 "%s" "$@"\n' "$TEST_ROOT/helpers/scripts/fnox_stub.py" >"$HOME/bin/fnox"
chmod +x "$HOME/bin/fnox"
export PATH="$HOME/bin:$PATH"
export FNOX_STUB_LOG="$PWD/fnox-calls.jsonl"

cat >mise.toml <<'TOML'
[secrets.fnox]

[tasks.migrate]
env.PGURL = "postgres://app:{{ secrets.DB_PASSWORD }}@db.internal/app"
run = '''
printf %s "$PGURL" >pgurl.out
printf %s "${DB_PASSWORD:-unset}" >pw.out
echo "url=$PGURL"
'''

[tasks.table]
env.PGURL = { value = "{{ secrets.DB_PASSWORD }}!" }
run = 'printf %s "$PGURL" >table.out'

[tasks.dep]
run = 'printf %s "${PGURL-unset}" >dep.out'

[tasks.parent]
depends = ["dep"]
run = 'true'
TOML

# the composed value reaches the task, the referenced key itself is not exported
assert_contains "mise run migrate </dev/null" "url=[redacted]"
assert_not_contains "mise run migrate </dev/null 2>&1" "s3cr3t"
assert "cat pgurl.out" "postgres://app:pw-s3cr3t-0007@db.internal/app"
assert "cat pw.out" "unset"
assert "mise run table </dev/null >/dev/null 2>&1; cat table.out" "pw-s3cr3t-0007!"

# the reference is the grant: one fnox call for the referenced key
rm -f fnox-calls.jsonl
mise run migrate </dev/null >/dev/null 2>&1
assert "jq -s '[.[]|select(.argv|index(\"--keys\"))]|length' fnox-calls.jsonl" "1"
assert "jq -r 'select(.argv|index(\"--keys\"))|.argv[(.argv|index(\"--keys\")) + 1]' fnox-calls.jsonl" "DB_PASSWORD"

# a task that does not run its dependency's env never sees the value, and nothing is resolved
rm -f fnox-calls.jsonl
mise run parent </dev/null >/dev/null 2>&1
assert "cat dep.out" "unset"
assert_fail "test -e fnox-calls.jsonl"

# the template, not a value, is what mise shows
assert_contains "mise tasks info migrate" "Secrets"
assert_contains "mise tasks info migrate --json" "{{ secrets.DB_PASSWORD }}"
assert_not_contains "mise tasks info migrate --json" "s3cr3t"
assert "mise tasks info migrate --json | jq -c .secrets" '["DB_PASSWORD"]'
assert "mise tasks ls --json | jq -c '.[]|select(.name==\"migrate\")|.secrets'" '["DB_PASSWORD"]'
assert_not_contains "mise tasks ls --json" "s3cr3t"
assert_contains "mise secrets ls" "migrate (env.PGURL)"
assert "mise secrets ls --json | jq -c '.keys[]|select(.key==\"DB_PASSWORD\")|.tasks[]|select(.task==\"migrate\")|[.via,.var]'" '["template","PGURL"]'
assert_not_contains "mise env --json 2>&1" "s3cr3t"

# the value is not part of the run, and listing the key as well exports it, which clashes
# with the env var that is built from it
cat >mise.toml <<'TOML'
[secrets.fnox]

[tasks.both]
secrets = ["DB_PASSWORD"]
env.DB_PASSWORD = "{{ secrets.DB_PASSWORD }}x"
run = "true"
TOML
assert_fail_contains "mise run both </dev/null" "DB_PASSWORD is both a secret and a mise env var"

# a key that is only referenced is not exported even when it is listed on another task
cat >mise.toml <<'TOML'
[secrets.fnox]

[tasks.same]
env.DB_PASSWORD = "{{ secrets.DB_PASSWORD }}"
run = 'printf %s "$DB_PASSWORD" >same.out'
TOML
mise run same </dev/null >/dev/null 2>&1
assert "cat same.out" "pw-s3cr3t-0007"

# T1: run is passed to the shell as an argument
cat >mise.toml <<'TOML'
[secrets.fnox]

[tasks.deploy]
run = './deploy.sh --key {{ secrets.DEPLOY_KEY }}'
TOML
assert_fail_contains "mise run deploy </dev/null" "{{ secrets.DEPLOY_KEY }} cannot be used in run"
assert_fail_contains "mise run deploy </dev/null" 'read it from the environment'
# shellcheck disable=SC2016
assert_fail_contains "mise run deploy </dev/null" "run = './deploy.sh --key \"\$DEPLOY_KEY\"'"
assert_fail_contains "mise run deploy </dev/null" "wrap it in {% raw %}...{% endraw %}"
cat >mise.toml <<'TOML'
[tasks.ci]
run = 'echo "{% raw %}x{{ secrets.GITHUB_TOKEN }}{% endraw %}"'
TOML
assert_contains "mise run ci </dev/null" 'x{{ secrets.GITHUB_TOKEN }}'

# T2: anywhere that is not a task's own env value
cat >mise.toml <<'TOML'
[secrets.fnox]

[tasks.a]
run = "true"
depends = [{ task = "b", env = { X = "{{ secrets.DEPLOY_KEY }}" } }]

[tasks.b]
run = "true"
TOML
assert_fail_contains "mise tasks ls" "{{ secrets.DEPLOY_KEY }} is only allowed in a task's own env values; found in tasks.a.depends[0].env.X"
cat >mise.toml <<'TOML'
[secrets.fnox]

[task_templates.t]
env.X = "{{ secrets.DEPLOY_KEY }}"

[tasks.a]
extends = "t"
run = "true"
TOML
assert_fail_contains "mise tasks ls" "found in task_templates.t.env.X"

# T3: no other template syntax in a value that uses secrets
cat >mise.toml <<'TOML'
[secrets.fnox]

[tasks.migrate]
env.PGURL = "postgres://app:{{ secrets.DB_PASSWORD | upper }}@db/app"
run = "true"
TOML
assert_fail_contains "mise tasks ls" "task migrate: env.PGURL mixes {{ secrets.* }} with other template syntax"

# T4: not in [env]
cat >mise.toml <<'TOML'
[secrets.fnox]

[env]
DATABASE_URL = "postgres://app:{{ secrets.DB_PASSWORD }}@db/app"
TOML
assert_fail_contains "mise env" "{{ secrets.DB_PASSWORD }} cannot be used in [env]"
assert_fail_contains "mise env" "Put it on the task that needs it"

# T5: another env value cannot read a value that is built when the task starts
cat >mise.toml <<'TOML'
[secrets.fnox]

[tasks.migrate]
env.PGURL = "postgres://app:{{ secrets.DB_PASSWORD }}@db/app"
env.OTHER = "{{ env.PGURL }}/x"
run = "true"
TOML
assert_fail_contains "mise run migrate </dev/null" "task migrate: env.OTHER uses {{ env.PGURL }}, but PGURL is rendered from secrets when the task starts"

# a key that is not valid or is reserved cannot be built
cat >mise.toml <<'TOML'
[secrets.fnox]

[tasks.migrate]
env."db.url" = "{{ secrets.DB_PASSWORD }}"
env.LANG = "{{ secrets.DB_PASSWORD }}"
run = "true"
TOML
assert_fail_contains "mise run migrate </dev/null" 'env."db.url" uses {{ secrets.* }} but is not a valid environment variable name'
assert_fail_contains "mise run migrate </dev/null" "env.LANG cannot be built from secrets; LANG is reserved by mise"

# an unknown reference is G1 and names the env value that asked for it
cat >mise.toml <<'TOML'
[secrets.fnox]

[tasks.migrate]
env.PGURL = "postgres://app:{{ secrets.DB_PASWORD }}@db/app"
run = "touch ran.out"
TOML
assert_fail_contains "mise run migrate </dev/null" "unknown secret DB_PASWORD"
assert_fail_contains "mise run migrate </dev/null" "Granted by: {{ secrets.DB_PASWORD }} in env.PGURL"
assert_fail_contains "mise run migrate </dev/null" "Did you mean DB_PASSWORD"
assert_fail "test -e ran.out"

# a listed key is exported next to the composite built from it
cat >mise.toml <<'TOML'
[secrets.fnox]

[tasks.listed]
secrets = ["DB_PASSWORD"]
env.PGURL = "p://{{ secrets.DB_PASSWORD }}@h"
run = 'printf "%s|%s" "$PGURL" "$DB_PASSWORD" >listed.out'
TOML
mise run listed </dev/null >/dev/null 2>&1
assert "cat listed.out" "p://pw-s3cr3t-0007@h|pw-s3cr3t-0007"

# a composed value follows normal env precedence
write_migrate() {
  cat >mise.toml <<'TOML'
[secrets.fnox]

[tasks.migrate]
env.PGURL = "postgres://app:{{ secrets.DB_PASSWORD }}@db/app"
run = 'printf %s "$PGURL" >order.out'

[tasks.migrate_t]
extends = "db"
env.PGURL = "postgres://app:{{ secrets.DB_PASSWORD }}@db/app"
run = 'printf %s "$PGURL" >order.out'

[task_templates.db]
env.PGURL = "postgres://localhost/dev"

[tasks.ci]
env.PGURL = "sqlite://parent"
run = [{ task = "migrate" }]

[tasks.testdb]
depends = [{ task = "migrate", env = { PGURL = "sqlite://test" } }]
run = "true"
TOML
}
write_migrate
composite="postgres://app:pw-s3cr3t-0007@db/app"
# a parent task's env is inherited first, so the composite overrides it
rm -f order.out
mise run ci </dev/null >/dev/null 2>&1
assert "cat order.out" "$composite"
# a dependency's env is applied after the task's own, so it wins and nothing is fetched
rm -f order.out fnox-calls.jsonl
mise run testdb </dev/null >/dev/null 2>&1
assert "cat order.out" "sqlite://test"
assert_fail "test -e fnox-calls.jsonl"
# a template's value is overridden by the task's composite
rm -f order.out
mise run migrate_t </dev/null >/dev/null 2>&1
assert "cat order.out" "$composite"
# a metadata-only block in a higher layer overlays the plain task, and its composite wins
cat >mise.toml <<'TOML'
[secrets.fnox]

[tasks.migrate]
env.PGURL = "plain"
run = 'printf %s "$PGURL" >order.out'
TOML
cat >mise.local.toml <<'TOML'
[tasks.migrate]
env.PGURL = "postgres://app:{{ secrets.DB_PASSWORD }}@db/app"
TOML
rm -f order.out fnox-calls.jsonl
mise run migrate </dev/null >/dev/null 2>&1
assert "cat order.out" "$composite"
assert "test -e fnox-calls.jsonl"
rm mise.local.toml
# a block layered over a file task still wins over a dependency's env, as plain values do
mkdir -p mise-tasks
cat >mise-tasks/migrate2 <<'SH'
#!/usr/bin/env bash
printf %s "$PGURL" >order.out
SH
chmod +x mise-tasks/migrate2
cat >mise.toml <<'TOML'
[secrets.fnox]

[tasks.migrate2]
env.PGURL = "postgres://app:{{ secrets.DB_PASSWORD }}@db/app"

[tasks.testdb2]
depends = [{ task = "migrate2", env = { PGURL = "sqlite://test" } }]
run = "true"
TOML
rm -f order.out fnox-calls.jsonl
mise run testdb2 </dev/null >/dev/null 2>&1
assert "cat order.out" "$composite"
assert "test -e fnox-calls.jsonl"
# an overlay that unsets the key wins: PGURL is unset and nothing is fetched
mkdir -p mise-tasks
cat >mise-tasks/ft2 <<'SH'
#!/usr/bin/env bash
#MISE env.PGURL="postgres://app:{{ secrets.DB_PASSWORD }}@db/app"
printf %s "${PGURL-unset}" >ft2.out
SH
chmod +x mise-tasks/ft2
cat >mise.toml <<'TOML'
[secrets.fnox]

[tasks.ft2]
env.PGURL = false
TOML
rm -f fnox-calls.jsonl
mise run ft2 </dev/null >/dev/null 2>&1
assert "cat ft2.out" "unset"
assert_fail "test -e fnox-calls.jsonl"
# [env], and the task's other env, still collide: the config sets the name too
cat >mise.toml <<'TOML'
[secrets.fnox]

[env]
PGURL = "postgres://localhost/dev"

[tasks.seed]
run = "touch seed.out"

[tasks.migrate]
env.PGURL = "postgres://app:{{ secrets.DB_PASSWORD }}@db/app"
run = "true"

[tasks.migrate_dep]
depends = ["seed"]
env.PGURL = "postgres://app:{{ secrets.DB_PASSWORD }}@db/app"
run = "true"
TOML
assert_fail_contains "mise run migrate </dev/null" "PGURL is both a secret and a mise env var"
# the preflight catches it before a dependency runs
assert_fail_contains "mise run migrate_dep </dev/null" "PGURL is both a secret and a mise env var"
assert_fail "test -e seed.out"

# another env value cannot read the composed variable, even with a shell $VAR
cat >mise.toml <<'TOML'
[secrets.fnox]

[tasks.migrate]
env.PGURL = "postgres://app:{{ secrets.DB_PASSWORD }}@db/app"
env.PSQL_URL = "$PGURL?sslmode=require"
run = "true"
TOML
# shellcheck disable=SC2016
assert_fail_contains "PGURL=postgres://localhost/dev mise run migrate </dev/null" 'env.PSQL_URL uses $PGURL, but PGURL is rendered from secrets when the task starts'

# $VAR expansion is not available next to a reference
cat >mise.toml <<'TOML'
[secrets.fnox]

[tasks.migrate]
env.PGURL = "postgres://$PGUSER:{{ secrets.DB_PASSWORD }}@db/app"
run = "true"
TOML
# shellcheck disable=SC2016
assert_fail_contains "mise run migrate </dev/null" 'env.PGURL uses $VAR expansion together with {{ secrets.* }}'

# a file secret cannot be composed
cat >mise.toml <<'TOML'
[secrets.fnox]

[tasks.migrate]
env.SA = "x{{ secrets.GCP_SA_JSON }}"
run = "true"
TOML
assert_fail_contains "mise run migrate </dev/null" "which fnox delivers as a file (as_file = true); a file secret cannot be composed into an env value"
assert_not_contains "mise run migrate </dev/null 2>&1 || true" "gcp-s3cr3t"

# --secrets-all gives referenced keys the C4 skips, not the listed-key errors
cat >mise.toml <<'TOML'
[secrets.fnox]

[tasks.same]
env.DB_PASSWORD = "{{ secrets.DB_PASSWORD }}"
run = 'printf %s "$DB_PASSWORD" >same.out'
TOML
rm -f same.out
mise run --secrets-all same </dev/null >/dev/null 2>&1
assert "cat same.out" "pw-s3cr3t-0007"
assert_contains "mise run --secrets-all same </dev/null 2>&1" "not granting DB_PASSWORD to task same: mise sets it for this task"
cat >mise.toml <<'TOML'
[secrets.fnox]

[env]
DB_PASSWORD = "dev"

[tasks.migrate]
env.PGURL = "postgres://app:{{ secrets.DB_PASSWORD }}@db/app"
run = 'printf "%s|%s" "$PGURL" "$DB_PASSWORD" >all.out'
TOML
assert_contains "mise run --secrets-all migrate </dev/null 2>&1" "not granting DB_PASSWORD to task migrate: mise sets it for this task"
assert "cat all.out" "$composite|dev"
cat >mise.toml <<'TOML'
[secrets.fnox]

[tasks.migrate]
deny_env = true
allow_env = ["PGURL"]
env.PGURL = "postgres://app:{{ secrets.DB_PASSWORD }}@db/app"
run = 'printf %s "$PGURL" >deny.out'
TOML
assert_contains "mise run --secrets-all migrate </dev/null 2>&1" "not granting DB_PASSWORD to task migrate: its sandbox denies env vars"
assert "cat deny.out" "$composite"

# --secrets-all still exports every injectable key under its own name, referenced ones included
cat >mise.toml <<'TOML'
[secrets.fnox]

[tasks.migrate]
env.PGURL = "postgres://app:{{ secrets.DB_PASSWORD }}@db/app"
run = 'printf "%s|%s" "$PGURL" "$DB_PASSWORD" >exported.out'
TOML
mise run --secrets-all migrate </dev/null >/dev/null 2>&1
assert "cat exported.out" "$composite|pw-s3cr3t-0007"

# $ syntax follows env_shell_expand; a $ before a reference never works
cat >mise.toml <<'TOML'
[secrets.fnox]

[tasks.migrate]
env.PGURL = "postgres://{{ secrets.DB_PASSWORD }}@h?x=$user"
run = 'printf %s "$PGURL" >dollar.out'
TOML
# shellcheck disable=SC2016
assert_fail_contains "mise run migrate </dev/null" 'env.PGURL uses $VAR expansion together with {{ secrets.* }}'
rm -f dollar.out
MISE_ENV_SHELL_EXPAND=false mise run migrate </dev/null >/dev/null 2>&1
# shellcheck disable=SC2016
assert "cat dollar.out" 'postgres://pw-s3cr3t-0007@h?x=$user'
cat >mise.toml <<'TOML'
[secrets.fnox]

[tasks.migrate]
env.GH_TOKEN = "${{ secrets.DB_PASSWORD }}"
run = "true"
TOML
# shellcheck disable=SC2016
assert_fail_contains "mise tasks ls" 'env.GH_TOKEN has ${{ secrets.DB_PASSWORD }}; drop the $'

# a composed file secret is refused before a dependency runs
cat >mise.toml <<'TOML'
[secrets.fnox]

[tasks.seed]
run = "touch seed.out"

[tasks.migrate]
depends = ["seed"]
env.SA = "x{{ secrets.GCP_SA_JSON }}"
run = "true"
TOML
assert_fail_contains "mise run migrate </dev/null" "which fnox delivers as a file (as_file = true)"
assert_fail "test -e seed.out"

# mise tasks validate reports a composed file secret too
cat >mise.toml <<'TOML'
[secrets.fnox]

[tasks.migrate]
env.SA = "x{{ secrets.GCP_SA_JSON }}"
run = "true"
TOML
assert_fail_contains "mise tasks validate" "which fnox delivers as a file (as_file = true)"

# an age-encrypted value is rendered after decryption, so it cannot read a composed key either
export MISE_EXPERIMENTAL=true
mise x age -- age-keygen -o age.txt 2>/dev/null
age_public=$(grep "# public key:" age.txt | cut -d: -f2 | tr -d ' ')
age_secret=$(grep "AGE-SECRET-KEY-" age.txt)
mkdir enc
(
  cd enc || exit 1
  : >mise.toml
  mise set OTHER='leak-marker-{{ env.PGURL }}' --age-encrypt --age-recipient "$age_public"
  mise set FINE='plain-text' --age-encrypt --age-recipient "$age_public"
)
{
  printf '[secrets.fnox]\n\n[tasks.migrate]\n'
  printf 'env.PGURL = "postgres://{{ secrets.DB_PASSWORD }}@h"\n'
  grep '^OTHER = ' enc/mise.toml | sed 's/^/env./'
  printf 'run = "true"\n\n[tasks.fine]\n'
  printf 'env.PGURL = "postgres://{{ secrets.DB_PASSWORD }}@h"\n'
  grep '^FINE = ' enc/mise.toml | sed 's/^/env./'
  printf 'run = %s\n' "'printf %s \"\$FINE\" >fine.out'"
} >mise.toml
export MISE_AGE_KEY="$age_secret"
rm -f fnox-calls.jsonl
assert_fail_contains "mise run migrate </dev/null" "env.OTHER uses {{ env.PGURL }}, but PGURL is rendered from secrets when the task starts"
assert_not_contains "mise run migrate </dev/null 2>&1 || true" "leak-marker"
assert_fail "test -e fnox-calls.jsonl"
mise run fine </dev/null >/dev/null 2>&1
assert "cat fine.out" "plain-text"

# a literal {{ env.X }} in a raw block is not a read of the composed variable
cat >mise.toml <<'TOML'
[secrets.fnox]

[tasks.migrate]
env.PGURL = "postgres://app:{{ secrets.DB_PASSWORD }}@db/app"
env.NOTE = "{% raw %}{{ env.PGURL }}{% endraw %}"
run = 'printf %s "$NOTE" >raw.out'
TOML
mise run migrate </dev/null >/dev/null 2>&1
assert "cat raw.out" "{{ env.PGURL }}"

# a get_env() whose name is computed may read any composed key, so it is refused
cat >mise.toml <<'TOML'
[secrets.fnox]

[vars]
KEY = "PGURL"

[tasks.migrate]
env.PGURL = "postgres://app:{{ secrets.DB_PASSWORD }}@db/app"
env.OTHER = "{{ get_env(name=vars.KEY) }}"
run = "true"
TOML
assert_fail_contains "mise run migrate </dev/null" "env.OTHER calls get_env() with a name that is not a string literal"
