#!/usr/bin/env bash

export AFFECTED_LOG="$PWD/affected.log"

cat <<'EOF' >.gitignore
affected.log
Cargo.lock
EOF

cat <<'EOF' >mise.toml
monorepo_root = true

[monorepo]
config_roots = ["crates/*"]

[task_config]
global_inputs = ["crates/core/global.txt"]
EOF

cat <<'EOF' >Cargo.toml
[workspace]
members = ["crates/app", "crates/core", "crates/other"]
resolver = "2"
EOF

mkdir -p crates/app/src crates/core/src crates/other/src
cat <<'EOF' >crates/app/Cargo.toml
[package]
name = "app"
version = "0.1.0"

[dependencies]
core = { path = "../core" }
EOF

for crate in core other; do
  cat <<EOF >"crates/$crate/Cargo.toml"
[package]
name = "$crate"
version = "0.1.0"
EOF
done

for crate in app core other; do
  cat <<EOF >"crates/$crate/mise.toml"
[tasks.build]
run = "echo $crate >> \"\$AFFECTED_LOG\""
EOF
  echo "$crate" >"crates/$crate/src/lib.rs"
done
cat <<'EOF' >>crates/app/mise.toml
depends = ["//crates/other:prepare"]
EOF
cat <<'EOF' >>crates/other/mise.toml

[tasks.prepare]
run = "echo other-prepare >> \"$AFFECTED_LOG\""
EOF
echo initial >crates/core/global.txt

git init -q
git config user.email test@example.com
git config user.name Test
git add .
git commit -qm initial

echo changed >>crates/core/src/lib.rs
git add .
git commit -qm core-change
assert_json "mise run --affected --affected-json build" '{
  "base": "HEAD~1",
  "head": "HEAD",
  "projects": [
    {
      "id": "cargo:app",
      "root": "crates/app",
      "reasons": [
        {
          "type": "dependent",
          "dependency": "cargo:core"
        }
      ]
    },
    {
      "id": "cargo:core",
      "root": "crates/core",
      "reasons": [
        {
          "type": "changed-path",
          "path": "crates/core/src/lib.rs"
        }
      ]
    }
  ],
  "tasks": [
    {
      "name": "//crates/app:build",
      "projects": ["cargo:app"]
    },
    {
      "name": "//crates/core:build",
      "projects": ["cargo:core"]
    }
  ]
}'
assert "test ! -e affected.log"
output=$(mise run --affected --affected-explain --jobs 1 build)
assert_contains "echo \"$output\"" "Affected projects (HEAD~1...HEAD):"
assert_contains "echo \"$output\"" "changed path: crates/core/src/lib.rs"
assert_contains "echo \"$output\"" "depends on affected project: cargo:core"
assert_contains "echo \"$output\"" "Affected tasks:"
assert_contains "echo \"$output\"" "affected project: cargo:app"
assert "sort affected.log" $'app\ncore\nother-prepare'

: >affected.log
echo changed >>crates/app/src/lib.rs
git add .
git commit -qm app-change
mise run --affected --affected-base HEAD~1 --affected-head HEAD --jobs 1 build
assert "sort affected.log" $'app\nother-prepare'

: >affected.log
echo changed >>crates/core/global.txt
git add .
git commit -qm global-change
mise run --affected --jobs 1 build
assert "sort affected.log" $'app\ncore\nother\nother-prepare'

: >affected.log
mise run --affected --affected-base HEAD --affected-head HEAD --jobs 1 build
assert "cat affected.log" ""

# A pnpm lockfile in a workspace without Node projects falls back to ordinary
# changed-path mapping instead of being swallowed as an empty provider result.
cat <<'EOF' >pnpm-lock.yaml
lockfileVersion: '9.0'
EOF
git add pnpm-lock.yaml
git commit -qm add-unrelated-pnpm-lockfile
cat <<'EOF' >pnpm-lock.yaml
lockfileVersion: '9.1'
EOF
git add pnpm-lock.yaml
git commit -qm change-unrelated-pnpm-lockfile
: >affected.log
output=$(mise run --affected --affected-explain --jobs 1 build)
assert_contains "echo \"$output\"" "workspace-global path: pnpm-lock.yaml"
assert "sort affected.log" $'app\ncore\nother\nother-prepare'

mkdir node-case
cd node-case
export AFFECTED_LOG="$PWD/affected.log"

cat <<'EOF' >.gitignore
affected.log
EOF
cat <<'EOF' >mise.toml
monorepo_root = true

[monorepo]
config_roots = ["packages/*"]
EOF
cat <<'EOF' >pnpm-workspace.yaml
packages:
  - packages/*
EOF
mkdir -p packages/app packages/lib packages/other
cat <<'EOF' >packages/app/package.json
{"name":"app","dependencies":{"lib":"workspace:*"}}
EOF
cat <<'EOF' >packages/lib/package.json
{"name":"lib"}
EOF
cat <<'EOF' >packages/other/package.json
{"name":"other"}
EOF
for package in app lib other; do
  cat <<EOF >"packages/$package/mise.toml"
[tasks.build]
run = "echo $package >> \"\$AFFECTED_LOG\""
EOF
done
cat <<'EOF' >pnpm-lock.yaml
lockfileVersion: '9.0'
importers:
  packages/app: {}
  packages/lib:
    dependencies:
      bar: {specifier: ^1.0.0, version: 1.0.0}
  packages/other: {}
packages:
  bar@1.0.0: {}
snapshots:
  bar@1.0.0: {}
EOF

git init -q
git config user.email test@example.com
git config user.name Test
git add .
git commit -qm initial
sed -i 's/1\.0\.0/2.0.0/g' pnpm-lock.yaml
git add pnpm-lock.yaml
git commit -qm lockfile-change

output=$(mise run --affected --affected-explain --jobs 1 build)
assert_contains "echo \"$output\"" "lockfile change: pnpm-lock.yaml"
assert_contains "echo \"$output\"" "depends on affected project: node:lib"
assert "sort affected.log" $'app\nlib'

# Untracked files count by default and under --affected-untracked, but not under
# --affected-committed. Ignored files never count.
echo notes >packages/other/notes.txt
echo ignored >affected.log.bak
echo '*.bak' >>.git/info/exclude
: >affected.log
mise run --affected --jobs 1 build
assert "sort affected.log" $'app\nlib\nother'
: >affected.log
mise run --affected --affected-untracked --jobs 1 build
assert "cat affected.log" "other"
: >affected.log
mise run --affected --affected-committed --jobs 1 build
assert "sort affected.log" $'app\nlib'
rm packages/other/notes.txt

# Staged and unstaged edits to tracked files count as uncommitted.
echo '# edit' >>packages/other/mise.toml
: >affected.log
mise run --affected --affected-uncommitted --jobs 1 build
assert "cat affected.log" "other"
git add packages/other/mise.toml
: >affected.log
mise run --affected --affected-uncommitted --jobs 1 build
assert "cat affected.log" "other"
: >affected.log
mise run --affected --affected-untracked --jobs 1 build
assert "test ! -s affected.log"
: >affected.log
mise run --affected --affected-uncommitted --affected-committed --jobs 1 build
assert "sort affected.log" $'app\nlib\nother'
git reset -q --hard

# A lockfile edited in the working tree is attributed to the projects it changes.
sed -i 's/2\.0\.0/3.0.0/g' pnpm-lock.yaml
output=$(mise run --affected --affected-uncommitted --affected-explain --jobs 1 build)
assert_contains "echo \"$output\"" "lockfile change: pnpm-lock.yaml"
git checkout -q pnpm-lock.yaml

# A committed lockfile change still counts when the working tree reverts it.
sed -i 's/2\.0\.0/1.0.0/g' pnpm-lock.yaml
: >affected.log
mise run --affected --jobs 1 build
assert "sort affected.log" $'app\nlib'
git checkout -q pnpm-lock.yaml

# The working tree only counts when head is the checkout.
: >affected.log
mise run --affected --affected-head HEAD~1 --affected-base HEAD~1 --jobs 1 build
assert "test ! -s affected.log"
assert_fail_contains "mise run --affected --affected-head HEAD~1 --affected-uncommitted build" "current checkout"

# The source env vars select sources like the flags, and setting one does not
# make a plain `mise run` demand --affected.
echo notes >packages/other/notes.txt
: >affected.log
MISE_AFFECTED_COMMITTED=1 mise run --affected --jobs 1 build
assert "sort affected.log" $'app\nlib'
: >affected.log
MISE_AFFECTED_UNTRACKED=1 mise run --affected --jobs 1 build
assert "cat affected.log" "other"
: >affected.log
MISE_AFFECTED_COMMITTED=1 MISE_AFFECTED_UNCOMMITTED=1 MISE_AFFECTED_UNTRACKED=1 mise run --jobs 1 //packages/other:build
assert "cat affected.log" "other"
rm packages/other/notes.txt

# core.autocrlf=true must not make a CRLF checkout look modified.
git config core.autocrlf true
printf 'one\n' >packages/other/crlf.txt
git add packages/other/crlf.txt
git commit -qm add-crlf
printf 'one\r\n' >packages/other/crlf.txt
: >affected.log
mise run --affected --affected-uncommitted --jobs 1 build
assert "test ! -s affected.log"
