#!/usr/bin/env bash

# Group sources resolve against dotfiles.root (~/.dotfiles by default).
DOTS="$HOME/.dotfiles"
mkdir -p "$DOTS/home/dot-config/kitty" "$DOTS/home/dot-config/app" "$DOTS/zsh" "$DOTS/work"
echo "bashrc" >"$DOTS/home/dot-bashrc"
echo "kitty" >"$DOTS/home/dot-config/kitty/kitty.conf"
echo "app" >"$DOTS/home/dot-config/app/config.toml"
echo "readme" >"$DOTS/home/README.md"
echo "zshrc" >"$DOTS/zsh/.zshrc"
echo "work" >"$DOTS/work/gitconfig"

cat <<EOF >mise.toml
[dotfile_groups.home]
root = "home"
dot_prefix = true
exclude = ["README.md"]
entries."~/.config/kitty" = { mode = "symlink" }

[dotfile_groups.zsh]
root = "zsh"

[dotfile_groups.work]
root = "work"
mode = "copy"
entries."~/.work.gitconfig" = { source = "gitconfig" }
EOF

# Every group applies when none are selected: each file of a tree is
# linked, the directory an entry names is linked whole, and an entry's
# source inside the tree is copied to its target only.
assert_succeed "mise dot apply --yes"
assert "readlink $HOME/.bashrc" "$DOTS/home/dot-bashrc"
assert "readlink $HOME/.zshrc" "$DOTS/zsh/.zshrc"
assert "readlink $HOME/.config/kitty" "$DOTS/home/dot-config/kitty"
assert "readlink $HOME/.config/app/config.toml" "$DOTS/home/dot-config/app/config.toml"
assert_fail "test -L $HOME/.config"
assert_fail "test -e $HOME/README.md"
assert "cat $HOME/.work.gitconfig" "work"
assert_fail "test -e $HOME/gitconfig"
assert_not_contains "mise dot status" "differs"

# Selecting groups leaves the others' files in place, reported as orphaned.
cat <<EOF >>mise.toml

[bootstrap]
dotfile_groups = ["home"]
EOF
assert_succeed "mise dot apply --yes"
assert "readlink $HOME/.zshrc" "$DOTS/zsh/.zshrc"
assert "cat $HOME/.work.gitconfig" "work"
assert_contains "mise dot status" "orphaned"
assert_contains "mise dot status --json" '"group": "zsh"'

# A deselected group's files can be unapplied by name.
assert_succeed "mise dot unapply --group work --yes"
assert_fail "test -e $HOME/.work.gitconfig"
assert "readlink $HOME/.zshrc" "$DOTS/zsh/.zshrc"

# An explicit prune removes the rest.
assert_fail_contains "mise dot apply --prune --yes ~/.bashrc" "cannot be combined with target arguments"
assert_succeed "test -L $HOME/.zshrc"
assert_contains "mise dot apply --prune --dry-run" "rm ~/.zshrc"
assert_succeed "test -L $HOME/.zshrc"
assert_succeed "mise dot apply --prune --yes"
assert_fail "test -e $HOME/.zshrc"
assert "readlink $HOME/.bashrc" "$DOTS/home/dot-bashrc"
assert_not_contains "mise dot status" "orphaned"

# A selected name nothing declares is reported.
cat <<EOF >mise.toml
[dotfile_groups.home]
root = "home"
dot_prefix = true
exclude = ["README.md"]
entries."~/.config/kitty" = { mode = "symlink" }

[bootstrap]
dotfile_groups = ["home", "hmoe"]
EOF
assert_contains "mise dot status 2>&1" "no [dotfile_groups] table declares it"

# Adding a file under a group tree captures it into the group's source with
# a dot- name, and writes no [dotfiles] entry.
echo "starship" >"$HOME/.config/starship.toml"
assert_succeed "mise dot add --yes $HOME/.config/starship.toml"
assert "cat $DOTS/home/dot-config/starship.toml" "starship"
assert "readlink $HOME/.config/starship.toml" "$DOTS/home/dot-config/starship.toml"
assert_not_contains "cat mise.toml" "starship"

# Two groups covering the same path need --group.
cat <<EOF >mise.toml
[dotfile_groups.home]
root = "home"
dot_prefix = true
exclude = ["README.md"]

[dotfile_groups.zsh]
root = "zsh"
EOF
echo "new" >"$HOME/.newrc"
assert_fail_contains "mise dot add --yes $HOME/.newrc" "choose one with --group"
assert_succeed "mise dot add --yes --group zsh $HOME/.newrc"
assert "cat $DOTS/zsh/.newrc" "new"
assert "readlink $HOME/.newrc" "$DOTS/zsh/.newrc"

# Selected groups that write the same target are an error.
echo "other" >"$DOTS/zsh/.bashrc"
assert_fail_contains "mise dot apply --yes" "group: zsh"
rm "$DOTS/zsh/.bashrc"

# So is an override that links a directory another group writes into.
mkdir -p "$DOTS/zsh/.config/kitty"
echo "theme" >"$DOTS/zsh/.config/kitty/theme.conf"
cat <<EOF >mise.toml
[dotfile_groups.home]
root = "home"
dot_prefix = true
exclude = ["README.md"]
entries."~/.config/kitty" = { mode = "symlink" }

[dotfile_groups.zsh]
root = "zsh"
EOF
assert_fail_contains "mise dot apply --yes" "conflicting"

# Entries describe parts of a group's tree with ordinary entry syntax. Their
# sources are found under the group root, and each is cut out of the walk.
mkdir -p "$DOTS/app/dot-config/app/themes" "$DOTS/app/conf"
echo "main" >"$DOTS/app/dot-config/app/main.toml"
echo "dark" >"$DOTS/app/dot-config/app/themes/dark.toml"
echo "light" >"$DOTS/app/dot-config/app/themes/light.toml"
echo "secret={{ env.HOME }}" >"$DOTS/app/conf/secret.tmpl"
echo "plain" >"$DOTS/app/dot-plainrc"
echo "stale" >"$HOME/.app-old.conf"
cat <<EOF >mise.toml
[dotfile_groups.app]
root = "app"
dot_prefix = true

[dotfile_groups.app.entries]
"~/.config/app/themes" = { mode = "copy" }
"~/.config/app/themes/light.toml" = { mode = "symlink" }
"~/.secret.conf" = { source = "conf/secret.tmpl", mode = "template" }
"~/.app-old.conf" = { mode = "absent" }
"~/.plainrc" = {}
EOF
assert_succeed "mise dot apply --yes"
assert "readlink $HOME/.config/app/main.toml" "$DOTS/app/dot-config/app/main.toml"
assert_fail "test -L $HOME/.config/app/themes/dark.toml"
assert "cat $HOME/.config/app/themes/dark.toml" "dark"
assert "readlink $HOME/.config/app/themes/light.toml" "$DOTS/app/dot-config/app/themes/light.toml"
assert "cat $HOME/.secret.conf" "secret=$HOME"
assert_fail "test -e $HOME/.app-old.conf"
# a file entry without a mode is linked, like the tree's files
assert "readlink $HOME/.plainrc" "$DOTS/app/dot-plainrc"
# an entry's source inside the tree is not also deployed by the walk
assert_fail "test -e $HOME/conf/secret.tmpl"

# An entry beneath a directory linked whole is refused before anything is
# written: that path is the file in the group's source.
cat <<EOF >mise.toml
[dotfile_groups.app]
root = "app"
dot_prefix = true
exclude = ["conf"]

[dotfile_groups.app.entries]
"~/.config/app/themes" = { mode = "symlink" }
"~/.config/app/themes/dark.toml" = { mode = "absent" }
EOF
assert_fail_contains "mise dot apply --yes --force" "conflicting"
assert "cat $DOTS/app/dot-config/app/themes/dark.toml" "dark"

# An entry must lie inside its group's target.
cat <<EOF >mise.toml
[dotfile_groups.app]
root = "app"
target = "~/.config"

[dotfile_groups.app.entries]
"~/.elsewhere" = { mode = "absent" }
EOF
assert_contains "mise dot status 2>&1" "must be inside the group's target"

# Every group names its root, and [dotfiles] entries take no group.
cat <<EOF >mise.toml
[dotfile_groups.rootless]

[dotfiles]
"~/.grouped" = { source = "$DOTS/zsh/.zshrc", group = "zsh" }
EOF
assert_contains "mise dot status 2>&1" "root is required"
assert_fail "test -e $HOME/.grouped"
assert_succeed "mise dot apply --yes"
assert_fail "test -e $HOME/.grouped"

# A walking entry with its own source reads like the group's tree
# (dot_prefix), and a nested entry's source inside it is not walked too.
mkdir -p "$DOTS/tool/tooltree"
echo "toolrc" >"$DOTS/tool/tooltree/dot-toolrc"
echo "gen={{ env.HOME }}" >"$DOTS/tool/tooltree/gen.tmpl"
cat <<EOF >mise.toml
[dotfile_groups.tool]
root = "tool"
dot_prefix = true

[dotfile_groups.tool.entries]
"~/.config/tool" = { source = "tooltree", mode = "symlink-each" }
"~/.config/tool/gen.conf" = { source = "tooltree/gen.tmpl", mode = "template" }
EOF
assert_succeed "mise dot apply --yes"
assert "readlink $HOME/.config/tool/.toolrc" "$DOTS/tool/tooltree/dot-toolrc"
assert "cat $HOME/.config/tool/gen.conf" "gen=$HOME"
assert_fail "test -e $HOME/.config/tool/gen.tmpl"
assert_fail "test -e $HOME/tooltree"

# A variant cannot deploy an entry outside its group's target.
cat <<EOF >mise.toml
[dotfile_groups.app]
root = "app"
target = "~/.config"

[dotfile_groups.app.entries]
"~/.config/tool.conf" = { mode = "absent", variants = [{ os = "linux", target = "~/.elsewhere" }, { default = true }] }
EOF
assert_contains "mise dot status 2>&1" "must be inside the group's target"
# nor onto the group's target itself
sed -i 's|target = "~/.elsewhere"|target = "~/.config"|' mise.toml
assert_contains "mise dot status 2>&1" "must be inside the group's target"

# Removal works under a target whose parent directory is a symlink.
mkdir -p "$HOME/realcfg" "$DOTS/linked"
echo "single" >"$DOTS/linked/single.conf"
ln -s "$HOME/realcfg" "$HOME/.cfg"
cat <<EOF >mise.toml
[dotfile_groups.linked]
root = "linked"
target = "~/.cfg"
mode = "copy"
entries."~/.cfg/single.conf" = { mode = "copy" }
EOF
assert_succeed "mise dot apply --yes"
assert "cat $HOME/realcfg/single.conf" "single"
assert_succeed "mise dot unapply --group linked --yes"
assert_fail "test -e $HOME/realcfg/single.conf"

# A declared group that fails to expand keeps its files: they are not
# orphaned, and a prune leaves them.
mkdir -p "$DOTS/keep"
echo "keep" >"$DOTS/keep/.keeprc"
cat <<EOF >mise.toml
[dotfile_groups.keep]
root = "keep"
EOF
assert_succeed "mise dot apply --yes"
cat <<EOF >mise.toml
[dotfile_groups.keep]
EOF
assert_not_contains "mise dot status 2>&1" ".keeprc"
assert_succeed "mise dot apply --prune --yes"
assert "readlink $HOME/.keeprc" "$DOTS/keep/.keeprc"
