#!/usr/bin/env bash

# JSON Schema checks that need a full draft 2020-12 validator: tombi does not
# enforce `not` or several cross-field constraints, so these use ajv, the same
# implementation repository lint runs. They share one file so node and ajv-cli
# are installed once rather than once per schema area.

mise use -g node@24 npm:ajv-cli@5.0.0
ajv_validate="mise x npm:ajv-cli@5.0.0 -- ajv validate --spec=draft2020 -s '$ROOT/schema/mise.json' -d"

# --- daemons

validate="$ajv_validate daemons.json"

cat >daemons.json <<'JSON'
{"daemons":{"api":{"task":"dev","args":[]}}}
JSON
assert_succeed "$validate"

for declaration in \
  '{"run":"exec api","args":[]}' \
  '{"preset":"postgres","version":"18","args":[]}'; do
  printf '{"daemons":{"api":%s}}\n' "$declaration" >daemons.json
  assert_fail "$validate"
done

# --- doctor

validate="$ajv_validate"
cat >doctor-valid.json <<'JSON'
{"doctor":{"checks":{"database":{"run":"pg_isready --quiet","description":"Database accepts connections","hint":"Start the services","timeout":"5s","dir":"../database","shell":"bash -c","os":["linux","macos"]}}}}
JSON
assert_succeed "$validate doctor-valid.json"
# Unknown container options stay valid, matching mise's forward compatibility.
cat >doctor-forward.json <<'JSON'
{"doctor":{"settings":{"future":true},"checks":{}}}
JSON
assert_succeed "$validate doctor-forward.json"
# Each invalid root-level block used to pass because doctor was misplaced
# inside task_dependency_item and was unvalidated at the root.
cat >doctor-invalid.json <<'JSON'
{"doctor":{"checks":{"database":{"hint":"missing command"}}}}
JSON
assert_fail "$validate doctor-invalid.json"
cat >doctor-invalid.json <<'JSON'
{"doctor":{"checks":{"database":{"run":"true","timeuot":"5s"}}}}
JSON
assert_fail "$validate doctor-invalid.json"
cat >doctor-invalid.json <<'JSON'
{"doctor":{"checks":{"database":{"run":"true","os":[]}}}}
JSON
assert_fail "$validate doctor-invalid.json"
cat >doctor-invalid.json <<'JSON'
{"tasks":{"test":{"depends":[{"task":"setup","doctor":{"checks":{}}}]}}}
JSON
assert_fail "$validate doctor-invalid.json"

# --- bootstrap file phases

validate="$ajv_validate phases.json"
cat >phases.json <<'JSON'
{"bootstrap":{"files":{"/etc/vendor.sources":{"source":"vendor.sources","phase":"pre-packages"}},"directories":{"/etc/vendor":{"phase":"post-packages"}}}}
JSON
assert_succeed "$validate"
cat >phases.json <<'JSON'
{"bootstrap":{"files":{"/etc/vendor.sources":{"content":"repo","phase":"invalid"}}}}
JSON
assert_fail "$validate"
cat >phases.json <<'JSON'
{"bootstrap":{"directories":{"/etc/vendor":{"phase":"invalid"}}}}
JSON
assert_fail "$validate"

# --- bootstrap file permissions only

validate="$ajv_validate permissions-only.json"
cat >permissions-only.json <<'JSON'
{"bootstrap":{"files":{"/etc/ssh/sshd_config":{"mode":"0600"}}}}
JSON
assert_succeed "$validate"
cat >permissions-only.json <<'JSON'
{"bootstrap":{"files":{"/etc/ssh/sshd_config":{"owner":"root","group":"root","state":"present"}}}}
JSON
assert_succeed "$validate"
cat >permissions-only.json <<'JSON'
{"bootstrap":{"files":{"/etc/obsolete.conf":{"state":"absent"}}}}
JSON
assert_succeed "$validate"
cat >permissions-only.json <<'JSON'
{"bootstrap":{"files":{"/etc/example.conf":{"content":"x","template":true,"replace":true}}}}
JSON
assert_succeed "$validate"
# A present file needs content or some permissions to manage.
cat >permissions-only.json <<'JSON'
{"bootstrap":{"files":{"/etc/ssh/sshd_config":{}}}}
JSON
assert_fail "$validate"
# Without content there is nothing to render, remove, or replace with.
cat >permissions-only.json <<'JSON'
{"bootstrap":{"files":{"/etc/ssh/sshd_config":{"mode":"0600","template":true}}}}
JSON
assert_fail "$validate"
cat >permissions-only.json <<'JSON'
{"bootstrap":{"files":{"/etc/ssh/sshd_config":{"mode":"0600","template":true,"remove_empty":true}}}}
JSON
assert_fail "$validate"
cat >permissions-only.json <<'JSON'
{"bootstrap":{"files":{"/etc/ssh/sshd_config":{"mode":"0600","replace":true}}}}
JSON
assert_fail "$validate"

# --- bootstrap file remove empty

validate="$ajv_validate remove-empty.json"
cat >remove-empty.json <<'JSON'
{"bootstrap":{"files":{"/etc/proxy.conf":{"content":"{{ vars.proxy }}","template":true,"remove_empty":true}}}}
JSON
assert_succeed "$validate"
cat >remove-empty.json <<'JSON'
{"bootstrap":{"files":{"/etc/proxy.conf":{"content":"","template":true,"remove_empty":true,"state":"present"}}}}
JSON
assert_succeed "$validate"
cat >remove-empty.json <<'JSON'
{"bootstrap":{"files":{"/etc/proxy.conf":{"content":"","remove_empty":false}}}}
JSON
assert_succeed "$validate"
# remove_empty requires a template.
cat >remove-empty.json <<'JSON'
{"bootstrap":{"files":{"/etc/proxy.conf":{"content":"","remove_empty":true}}}}
JSON
assert_fail "$validate"
cat >remove-empty.json <<'JSON'
{"bootstrap":{"files":{"/etc/proxy.conf":{"content":"","template":false,"remove_empty":true}}}}
JSON
assert_fail "$validate"
# remove_empty applies only to present files.
cat >remove-empty.json <<'JSON'
{"bootstrap":{"files":{"/etc/proxy.conf":{"template":true,"remove_empty":true,"state":"absent"}}}}
JSON
assert_fail "$validate"

# --- bootstrap services (the tombi checks stay in test_schema_bootstrap_services)
# Tombi does not enforce JSON Schema's `not`, so this cross-field constraint
# lives here.
printf '{"bootstrap":{"services":{"worker":{"builtin":"history-watch","command":"worker"}}}}\n' >services-invalid.json
assert_fail "$ajv_validate services-invalid.json"
printf '{"bootstrap":{"services":{"worker":{"builtin":"history-watch"}}}}\n' >services-valid.json
assert_succeed "$ajv_validate services-valid.json"

# --- backend-specific tool options (typed only for tools of that backend)
for tools in \
  '"spinel:tobi/try":"1.10.1"' \
  '"spinel:tobi/try":{"version":"1.10.1","entrypoint":"try.rb","bin":"try","tag_prefix":"v","spinel":"/opt/spinel"}' \
  '"spinel:tobi/try":[{"version":"1.10.1","entrypoint":"try.rb","source_ref":"be566829856ca6bd22963e38a28ffb5efa24c7de"}]' \
  '"cargo:foo":{"version":"1","entrypoint":5}' \
  '"github:cli/cli":{"version":"2","asset_pattern":"gh_*","no_app":true,"strip_components":1,"rename_exe":{"gh-*":"gh"}}' \
  '"github:cli/cli":{"version":"2","no_app":"true","strip_components":"1"}' \
  '"github:cli/cli":{"version":"2","platforms":{"linux-x64":{"asset_pattern":"a","url":"https://x/y"}}}' \
  '"gitlab:o/r":{"version":"1","api_url":"https://git.example.com/api/v4"}' \
  '"http:tool":{"version":"1","format":"tar.gz","platforms":{"macos-arm64":{"url":"https://x","checksum":"sha256:ab"}}}' \
  '"aqua:o/r":{"version":"1","libc":"musl","vars":{"a":"b"}}' \
  '"cargo:ripgrep":{"version":"1","features":["a"],"locked":false,"platforms":{"linux-x64":{"bin":"rg"}}}' \
  '"npm:x":{"version":"1","allow_builds":["a"],"allow_exotic_deps":true}' \
  '"npm:x":{"version":"1","allow_low_downloads":"TRUE"}' \
  '"pipx:x":{"version":"1","extras":["a"],"uvx":false,"dependency_prereleases":"allow"}' \
  '"python":{"version":"3","patch_sysconfig":"false"}' \
  '"java":{"version":"21","release_type":"ea"}' \
  '"rust":{"version":"1","profile":"minimal","components":"a,b","mr_boxington":true}' \
  '"dotnet":{"version":"8","runtime":"aspnetcore"}' \
  '"dotnet:Some.Tool":{"version":"1","runtime":"anything"}' \
  '"node":{"version":"22","prerelease":true,"lazy_bins":"node"}'; do
  printf '{"tools":{%s}}\n' "$tools" >tool-options-valid.json
  assert_succeed "$ajv_validate tool-options-valid.json"
done
for tools in \
  '"spinel:tobi/try":{"version":"1","entrypoint":5}' \
  '"spinel:tobi/try":{"version":"1","source_ref":"abc"}' \
  '"spinel:tobi/try":[{"version":"1","bin":false}]' \
  '"github:cli/cli":{"version":"2","asset_pattern":5}' \
  '"github:cli/cli":{"version":"2","no_app":"maybe"}' \
  '"github:cli/cli":{"version":"2","platforms":{"linux-x64":{"asset_pattern":5}}}' \
  '"github:cli/cli":{"version":"2","platforms":{"linux":{"asset_pattern":"a"}}}' \
  '"http:tool":{"version":"1","format":"rar5"}' \
  '"aqua:o/r":{"version":"1","libc":"uclibc"}' \
  '"cargo:ripgrep":{"version":"1","features":5}' \
  '"npm:x":{"version":"1","allow_low_downloads":"sure"}' \
  '"npm:x":{"version":"1","allow_low_downloads":"1"}' \
  '"npm:x":{"version":"1","allow_low_downloads":" true "}' \
  '"npm:x":{"version":"1","allow_low_downloads":1}' \
  '"python":{"version":"3","patch_sysconfig":"nah"}' \
  '"java":{"version":"21","release_type":"stable"}' \
  '"dotnet-core":{"version":"8","runtime":"sdk"}' \
  '"node":{"version":"22","lazy_bins":["a/b"]}'; do
  printf '{"tools":{%s}}\n' "$tools" >tool-options-invalid.json
  assert_fail "$ajv_validate tool-options-invalid.json"
done

# Task tool tables use the same typed options.
printf '{"tasks":{"build":{"run":"make","tools":{"github:cli/cli":{"version":"2","asset_pattern":"a"}}}}}\n' >task-tools-valid.json
assert_succeed "$ajv_validate task-tools-valid.json"
printf '{"tasks":{"build":{"run":"make","tools":{"github:cli/cli":{"version":"2","asset_pattern":5}}}}}\n' >task-tools-invalid.json
assert_fail "$ajv_validate task-tools-invalid.json"

# Deprecated root key that mise still accepts with a warning.
printf '{"experimental_monorepo_root":true}\n' >legacy-monorepo-root.json
assert_succeed "$ajv_validate legacy-monorepo-root.json"
