#!/usr/bin/env bash

# `mise run --secrets` and `--secrets-all` grant only the tasks named on the command line.

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.build]
run = 'printf %s "${DATABASE_URL-unset}" >build.out'

[tasks.deploy]
depends = ["build"]
run = 'printf %s "${DATABASE_URL-unset}" >deploy-db.out'

[tasks.show]
run = '''
printf %s "${STRIPE_KEY-unset}" >show-stripe.out
printf %s "${AWS_ACCESS_KEY_ID-unset}" >show-aws.out
printf %s "${SIGNING_KEY-unset}" >show-signing.out
printf %s "${EXTRA_KEY-unset}" >show-extra.out
'''

[tasks.args]
run = 'printf "%s|" >args.out'

[tasks.a]
run = 'true'

[tasks.b]
run = 'true'

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

[tasks.outer]
run = [{ task = "inner" }]
TOML

resolves() {
  if [[ -f fnox-calls.jsonl ]]; then
    jq -s '[.[]|select(.argv|index("--keys"))]|length' fnox-calls.jsonl
  else
    echo 0
  fi
}
fresh() {
  rm -f fnox-calls.jsonl build.out deploy-db.out args.out inner.out show-*.out
}

# only the named task receives the key, not its dependency
mise run --secrets DATABASE_URL deploy </dev/null >/dev/null 2>&1
assert "cat deploy-db.out" "postgres://app:db-s3cr3t-0002@db/app"
assert "cat build.out" "unset"
assert "jq -r 'select(.argv|index(\"--keys\"))|.argv[-1]' fnox-calls.jsonl" "DATABASE_URL"

# naming the dependency too grants it
fresh
mise run --secrets DATABASE_URL build ::: deploy </dev/null >/dev/null 2>&1
assert "cat build.out" "postgres://app:db-s3cr3t-0002@db/app"
assert "cat deploy-db.out" "postgres://app:db-s3cr3t-0002@db/app"

# a glob names every task it selects
fresh
mise run --secrets DATABASE_URL 'dep*' </dev/null >/dev/null 2>&1
assert "cat deploy-db.out" "postgres://app:db-s3cr3t-0002@db/app"
assert "cat build.out" "unset"

# a task that a run entry starts is never named
fresh
mise run --secrets DATABASE_URL outer </dev/null >/dev/null 2>&1
assert "cat inner.out" "unset"

# --secrets-all gives every injectable key, in one call that names no keys
fresh
mise run --secrets-all show </dev/null >/dev/null 2>&1
assert "cat show-stripe.out" "stripe-s3cr3t-0003"
assert "cat show-aws.out" "aws-s3cr3t-0009"
assert_not_contains "cat show-signing.out" "signing-s3cr3t-0008"
assert "jq -s '[.[]|select(.argv|index(\"--for\"))|select((.argv|index(\"--describe\"))|not)]|length' fnox-calls.jsonl" "1"
assert "jq -s '[.[]|select((.argv|index(\"--for\")) and ((.argv|index(\"--describe\"))|not) and (.argv|index(\"--keys\")))]|length' fnox-calls.jsonl" "0"

# a source that returns more than the catalog describes cannot widen the grant
fresh
FNOX_STUB_MODE=extra mise run --secrets-all show </dev/null >/dev/null 2>&1
assert "cat show-extra.out" "unset"
assert_not_contains "cat show-signing.out" "signing-s3cr3t-0008"

# two named tasks share one call
fresh
mise run --secrets-all a ::: b </dev/null >/dev/null 2>&1
assert "jq -s '[.[]|select((.argv|index(\"--for\")) and ((.argv|index(\"--describe\"))|not))]|length' fnox-calls.jsonl" "1"
assert "jq -s '[.[]|select(.argv|index(\"--keys\"))]|length' fnox-calls.jsonl" "0"

# a dry run resolves nothing
fresh
assert_contains "mise run -n --secrets-all show </dev/null 2>&1" "all injectable secrets"
[[ ! -e fnox-calls.jsonl ]] || fail "a dry run must not run fnox"

# mise tasks run takes the same flags
fresh
mise tasks run --secrets DATABASE_URL deploy </dev/null >/dev/null 2>&1
assert "cat deploy-db.out" "postgres://app:db-s3cr3t-0002@db/app"

# after the task name the flag belongs to the task, and mise warns
fresh
assert_contains "mise run args --secrets X </dev/null 2>&1" "--secrets after the task name is passed to args as an argument"
assert "cat args.out" "--secrets|X|"
[[ "$(resolves)" == 0 ]] || fail "unexpected fnox resolution"
# arguments after -- are the task's and are not a mistake
fresh
assert_not_contains "mise run args -- --secrets X </dev/null 2>&1" "after the task name"
assert "cat args.out" "--secrets|X|"
[[ "$(resolves)" == 0 ]] || fail "unexpected fnox resolution"

# a bad key fails before any task runs, and says where it was granted
fresh
assert_fail_contains "mise run --secrets NOPE deploy </dev/null" "unknown secret NOPE"
assert_fail_contains "mise run --secrets NOPE deploy </dev/null" "Granted by: --secrets"
[[ ! -e build.out ]] || fail "build ran before the grant was checked"
assert_fail_contains "mise run --secrets SIGNING_KEY deploy </dev/null" "SIGNING_KEY cannot be injected"
assert_fail_contains "mise run --secrets 'AWS_*' deploy </dev/null" "wildcards are not supported"
assert_fail_contains "mise run --secrets PATH deploy </dev/null" "reserved by mise"

# the flags conflict with each other and with --affected
assert_fail_contains "mise run --secrets-all --affected </dev/null" "cannot be used with"
assert_fail_contains "mise run --secrets-all --secrets DATABASE_URL deploy </dev/null" "cannot be used with"
assert_fail_contains "mise run --secrets DATABASE_URL --affected deploy </dev/null" "cannot be used with"

# a task from global config cannot list secrets, but a person can grant it one
mkdir -p "$MISE_CONFIG_DIR"
# a global task runs in the config's directory, so it writes to an absolute path
cat >"$MISE_CONFIG_DIR/config.toml" <<TOML
[tasks.gtask]
run = 'printf %s "\${DEPLOY_KEY-unset}" >$PWD/g.out'

[tasks.glisted]
secrets = ["DEPLOY_KEY"]
run = 'true'
TOML
mise run --secrets DEPLOY_KEY gtask </dev/null >/dev/null 2>&1
assert "cat g.out" "deploy-s3cr3t-0001"
assert_fail_contains "mise run glisted </dev/null" "which is not project config"
assert_fail_contains "mise run glisted </dev/null" "grant for one run with mise run --secrets-all glisted"
rm "$MISE_CONFIG_DIR/config.toml"

# --secrets-all skips a key mise sets for the task instead of failing
cat >mise.toml <<'TOML'
[secrets.fnox]

[env]
DATABASE_URL = "from-mise"

[tasks.show]
run = '''
printf %s "${DATABASE_URL-unset}" >db.out
printf %s "${STRIPE_KEY-unset}" >stripe.out
'''
TOML
assert_contains "mise run --secrets-all show </dev/null 2>&1" "not granting DATABASE_URL to task show: mise sets it for this task"
assert "cat db.out" "from-mise"
assert "cat stripe.out" "stripe-s3cr3t-0003"
# but naming it is an error
assert_fail_contains "mise run --secrets DATABASE_URL show </dev/null" "DATABASE_URL is both a secret and a mise env var"
# and a sandbox that drops the keys skips them too
assert_contains "mise run --secrets-all --deny-env show </dev/null 2>&1" "its sandbox denies env vars"

# a key a template reads cannot reach it: --secrets-all skips it, --secrets refuses
cat >mise.toml <<'TOML'
[secrets.fnox]

[tasks.pay]
run = 'echo "tpl={{ env.STRIPE_KEY | default(value="none") }} env=${STRIPE_KEY:-unset}"'
TOML
assert_contains "mise run --secrets-all pay </dev/null 2>&1" "not granting STRIPE_KEY to task pay: {{ env.STRIPE_KEY }} in its run or env cannot see the secret"
assert_contains "mise run --secrets-all pay </dev/null 2>&1" "tpl=none env=unset"
assert_fail_contains "mise run --secrets STRIPE_KEY pay </dev/null" "{{ env.STRIPE_KEY }} in run cannot see the secret"

# a value with a NUL byte cannot be an environment variable: the task fails with a named error
cat >mise.toml <<'TOML'
[secrets.fnox]

[tasks.nul]
run = 'true'
TOML
rm -f nul.out
FNOX_STUB_MODE=nul mise run --secrets DEPLOY_KEY nul >nul.out 2>&1 </dev/null && fail "a task with a NUL value succeeded"
assert_contains "cat nul.out" "task nul: DEPLOY_KEY contains a NUL byte"
assert_not_contains "cat nul.out" "s3cr3t"
assert_not_contains "cat nul.out" "panicked"

# a named task that only starts other tasks has nothing to inject into: a warning, no fnox
cat >mise.toml <<'TOML'
[secrets.fnox]

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

[tasks.parent]
run = [{ task = "child" }]
TOML
rm -f fnox-calls.jsonl child.out
assert_contains "mise run --secrets DATABASE_URL parent </dev/null 2>&1" "task parent starts no process of its own"
assert "cat child.out" "unset"
[[ ! -e fnox-calls.jsonl ]] || fail "fnox must not run for a task that only orchestrates"
