use crate::common::{TestContext, cmd_snapshot, git_cmd};
use assert_cmd::assert::OutputAssertExt;
use assert_fs::assert::PathAssert;
use assert_fs::fixture::{FileWriteStr, PathChild, PathCreateDir};
use indoc::indoc;
use insta::assert_snapshot;
use prek_consts::PRE_COMMIT_CONFIG_YAML;
use prek_consts::env_vars::EnvVars;

mod common;

#[test]
fn install() -> anyhow::Result<()> {
    let context = TestContext::new();
    context.init_project();

    // Install `prek` hook.
    cmd_snapshot!(context.filters(), context.install(), @r#"
    success: true
    exit_code: 0
    ----- stdout -----
    prek installed at `.git/hooks/pre-commit`

    ----- stderr -----
    "#);

    insta::with_settings!(
        { filters => context.filters() },
        {
            assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
            #!/bin/sh
            # File generated by prek: https://github.com/j178/prek
            # ID: 182c10f181da4464a3eec51b83331688

            HERE="$(cd "$(dirname "$0")" && pwd)"
            PREK="[CURRENT_EXE]"

            # Check if the full path to prek is executable, otherwise fallback to PATH
            if [ ! -x "$PREK" ]; then
                PREK="prek"
            fi

            exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit -- "$@"
            "#);
        }
    );

    // Install `pre-commit` and `post-commit` hook.
    context
        .work_dir()
        .child(".git/hooks/pre-commit")
        .write_str("#!/bin/sh\necho 'pre-commit'\n")?;

    cmd_snapshot!(context.filters(), context.install().arg("--hook-type").arg("pre-commit").arg("--hook-type").arg("post-commit"), @r"
    success: true
    exit_code: 0
    ----- stdout -----
    Hook already exists at `.git/hooks/pre-commit`, moved it to `.git/hooks/pre-commit.legacy`
    Migration mode: prek will also run legacy hook `.git/hooks/pre-commit.legacy`. Use `--overwrite` to remove legacy hooks.
    prek installed at `.git/hooks/pre-commit`
    prek installed at `.git/hooks/post-commit`

    ----- stderr -----
    ");
    insta::with_settings!(
        { filters => context.filters() },
        {
            assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
            #!/bin/sh
            # File generated by prek: https://github.com/j178/prek
            # ID: 182c10f181da4464a3eec51b83331688

            HERE="$(cd "$(dirname "$0")" && pwd)"
            PREK="[CURRENT_EXE]"

            # Check if the full path to prek is executable, otherwise fallback to PATH
            if [ ! -x "$PREK" ]; then
                PREK="prek"
            fi

            exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit -- "$@"
            "#);
        }
    );

    assert_snapshot!(context.read(".git/hooks/pre-commit.legacy"), @r##"
    #!/bin/sh
    echo 'pre-commit'
    "##);

    insta::with_settings!(
        { filters => context.filters() },
        {
            assert_snapshot!(context.read(".git/hooks/post-commit"), @r#"
            #!/bin/sh
            # File generated by prek: https://github.com/j178/prek
            # ID: 182c10f181da4464a3eec51b83331688

            HERE="$(cd "$(dirname "$0")" && pwd)"
            PREK="[CURRENT_EXE]"

            # Check if the full path to prek is executable, otherwise fallback to PATH
            if [ ! -x "$PREK" ]; then
                PREK="prek"
            fi

            exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=post-commit -- "$@"
            "#);
        }
    );

    // Overwrite existing hooks.
    cmd_snapshot!(context.filters(), context.install().arg("-t").arg("pre-commit").arg("--hook-type").arg("post-commit").arg("--overwrite"), @r#"
    success: true
    exit_code: 0
    ----- stdout -----
    Overwriting existing hook at `.git/hooks/pre-commit`
    prek installed at `.git/hooks/pre-commit`
    Overwriting existing hook at `.git/hooks/post-commit`
    prek installed at `.git/hooks/post-commit`

    ----- stderr -----
    "#);

    insta::with_settings!(
        { filters => context.filters() },
        {
            assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
            #!/bin/sh
            # File generated by prek: https://github.com/j178/prek
            # ID: 182c10f181da4464a3eec51b83331688

            HERE="$(cd "$(dirname "$0")" && pwd)"
            PREK="[CURRENT_EXE]"

            # Check if the full path to prek is executable, otherwise fallback to PATH
            if [ ! -x "$PREK" ]; then
                PREK="prek"
            fi

            exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit -- "$@"
            "#);
        }
    );
    insta::with_settings!(
        { filters => context.filters() },
        {
            assert_snapshot!(context.read(".git/hooks/post-commit"), @r#"
            #!/bin/sh
            # File generated by prek: https://github.com/j178/prek
            # ID: 182c10f181da4464a3eec51b83331688

            HERE="$(cd "$(dirname "$0")" && pwd)"
            PREK="[CURRENT_EXE]"

            # Check if the full path to prek is executable, otherwise fallback to PATH
            if [ ! -x "$PREK" ]; then
                PREK="prek"
            fi

            exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=post-commit -- "$@"
            "#);
        }
    );

    Ok(())
}

#[test]
fn install_with_git_dir() {
    let context = TestContext::new();
    context.init_project();

    cmd_snapshot!(context.filters(), context.install().arg("--git-dir").arg("custom-git-dir"), @r#"
    success: true
    exit_code: 0
    ----- stdout -----
    prek installed at `custom-git-dir/hooks/pre-commit`

    ----- stderr -----
    "#);

    context
        .work_dir()
        .child(".git/hooks/pre-commit")
        .assert(predicates::path::missing());
    context
        .work_dir()
        .child("custom-git-dir/hooks/pre-commit")
        .assert(predicates::path::exists());

    insta::with_settings!(
        { filters => context.filters() },
        {
            assert_snapshot!(context.read("custom-git-dir/hooks/pre-commit"), @r#"
            #!/bin/sh
            # File generated by prek: https://github.com/j178/prek
            # ID: 182c10f181da4464a3eec51b83331688

            HERE="$(cd "$(dirname "$0")" && pwd)"
            PREK="[CURRENT_EXE]"

            # Check if the full path to prek is executable, otherwise fallback to PATH
            if [ ! -x "$PREK" ]; then
                PREK="prek"
            fi

            exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit -- "$@"
            "#);
        }
    );
}

#[test]
fn install_with_local_hooks_path_installs_to_configured_directory() {
    let context = TestContext::new();
    context.init_project();

    git_cmd(context.work_dir())
        .args(["config", "core.hooksPath", "custom-hooks"])
        .assert()
        .success();

    cmd_snapshot!(context.filters(), context.install(), @r#"
    success: true
    exit_code: 0
    ----- stdout -----
    prek installed at `custom-hooks/pre-commit`

    ----- stderr -----
    "#);

    context
        .work_dir()
        .child(".git/hooks/pre-commit")
        .assert(predicates::path::missing());
    context
        .work_dir()
        .child("custom-hooks/pre-commit")
        .assert(predicates::path::exists());

    insta::with_settings!(
        { filters => context.filters() },
        {
            assert_snapshot!(context.read("custom-hooks/pre-commit"), @r#"
            #!/bin/sh
            # File generated by prek: https://github.com/j178/prek
            # ID: 182c10f181da4464a3eec51b83331688

            HERE="$(cd "$(dirname "$0")" && pwd)"
            PREK="[CURRENT_EXE]"

            # Check if the full path to prek is executable, otherwise fallback to PATH
            if [ ! -x "$PREK" ]; then
                PREK="prek"
            fi

            exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit -- "$@"
            "#);
        }
    );
}

#[test]
fn install_with_git_dir_allows_external_hooks_path_set() {
    let context = TestContext::new();
    context.init_project();

    let global_gitconfig = context.work_dir().join("global.gitconfig");
    git_cmd(context.work_dir())
        .env("GIT_CONFIG_GLOBAL", &global_gitconfig)
        .args(["config", "--global", "core.hooksPath", "custom-hooks"])
        .assert()
        .success();

    let mut install = context.install();
    install.env("GIT_CONFIG_GLOBAL", &global_gitconfig);
    cmd_snapshot!(context.filters(), install, @"
    success: false
    exit_code: 2
    ----- stdout -----

    ----- stderr -----
    error: Refusing to install hooks because `core.hooksPath` is configured outside this repository.

    note: Git will execute hooks from the configured global/system hooks directory, not from this repository's hooks directory.

    hint: Remove the global/system setting, or move `core.hooksPath` into repo scope for this repository instead.
      git config --unset-all --global core.hooksPath
      git config --unset-all --system core.hooksPath
      git config --local core.hooksPath <path>
    ");

    let mut install = context.install();
    install
        .arg("--git-dir")
        .arg(".git")
        .env("GIT_CONFIG_GLOBAL", &global_gitconfig);
    cmd_snapshot!(context.filters(), install, @r#"
    success: true
    exit_code: 0
    ----- stdout -----
    prek installed at `.git/hooks/pre-commit`

    ----- stderr -----
    "#);
}

#[test]
fn install_refuses_empty_external_hooks_path_set() {
    let context = TestContext::new();
    context.init_project();

    let global_gitconfig = context.work_dir().join("global.gitconfig");
    git_cmd(context.work_dir())
        .env("GIT_CONFIG_GLOBAL", &global_gitconfig)
        .args(["config", "--global", "core.hooksPath", ""])
        .assert()
        .success();

    let mut install = context.install();
    install.env("GIT_CONFIG_GLOBAL", &global_gitconfig);
    cmd_snapshot!(context.filters(), install, @"
    success: false
    exit_code: 2
    ----- stdout -----

    ----- stderr -----
    error: Refusing to install hooks because `core.hooksPath` is configured outside this repository.

    note: Git will execute hooks from the configured global/system hooks directory, not from this repository's hooks directory.

    hint: Remove the global/system setting, or move `core.hooksPath` into repo scope for this repository instead.
      git config --unset-all --global core.hooksPath
      git config --unset-all --system core.hooksPath
      git config --local core.hooksPath <path>
    ");
}

#[test]
fn install_refuses_empty_local_hooks_path_set() {
    let context = TestContext::new();
    context.init_project();

    git_cmd(context.work_dir())
        .args(["config", "core.hooksPath", ""])
        .assert()
        .success();

    cmd_snapshot!(context.filters(), context.install(), @r#"
    success: false
    exit_code: 2
    ----- stdout -----

    ----- stderr -----
    error: Git resolved hooks directory to the current directory (`.`). Unset `core.hooksPath` or set it to a real directory path.
    "#);
}

#[test]
fn install_with_dot_hooks_path_installs_to_repo_root() {
    let context = TestContext::new();
    context.init_project();

    git_cmd(context.work_dir())
        .args(["config", "core.hooksPath", "."])
        .assert()
        .success();

    context.install().assert().success();

    context
        .work_dir()
        .child(".git/hooks/pre-commit")
        .assert(predicates::path::missing());
    context
        .work_dir()
        .child("pre-commit")
        .assert(predicates::path::exists());
}

#[test]
fn install_with_included_local_hooks_path_installs_to_configured_directory() -> anyhow::Result<()> {
    let context = TestContext::new();
    context.init_project();

    context
        .work_dir()
        .child("included-hooks.cfg")
        .write_str(indoc! {r"
        [core]
            hooksPath = custom-hooks
    "})?;

    git_cmd(context.work_dir())
        .args(["config", "--local", "include.path", "../included-hooks.cfg"])
        .assert()
        .success();

    context.install().assert().success();

    context
        .work_dir()
        .child(".git/hooks/pre-commit")
        .assert(predicates::path::missing());
    context
        .work_dir()
        .child("custom-hooks/pre-commit")
        .assert(predicates::path::exists());

    Ok(())
}

#[test]
fn install_with_worktree_hooks_path_installs_to_configured_directory() -> anyhow::Result<()> {
    let context = TestContext::new();
    context.init_project();
    context.work_dir().child("README.md").write_str("hello\n")?;
    context.git_add(".");
    context.git_commit("Initial commit");

    git_cmd(context.work_dir())
        .args(["config", "extensions.worktreeConfig", "true"])
        .assert()
        .success();
    git_cmd(context.work_dir())
        .args(["worktree", "add", "worktree", "HEAD"])
        .assert()
        .success();

    let worktree = context.work_dir().child("worktree");
    let output = git_cmd(&worktree)
        .args(["rev-parse", "--path-format=absolute", "--git-dir"])
        .output()?;
    let worktree_git_dir = String::from_utf8(output.stdout)?.trim().to_string();
    let worktree_hooks = std::path::PathBuf::from(&worktree_git_dir).join("hooks");
    let worktree_hooks_str = worktree_hooks.display().to_string();

    git_cmd(&worktree)
        .args([
            "config",
            "--worktree",
            "core.hooksPath",
            &worktree_hooks_str,
        ])
        .assert()
        .success();

    let mut install = context.install();
    install.current_dir(&worktree);
    install.assert().success();

    context
        .work_dir()
        .child(".git/hooks/pre-commit")
        .assert(predicates::path::missing());
    assert!(
        worktree_hooks.join("pre-commit").exists(),
        "expected hook to be installed into {}",
        worktree_hooks.display()
    );

    Ok(())
}

/// Hook permissions should be standard when `core.sharedRepository` is not set.
#[test]
#[cfg(unix)]
fn install_uses_standard_permissions_by_default() {
    use std::os::unix::fs::PermissionsExt;

    let context = TestContext::new();
    context.init_project();

    context.install().assert().success();

    // Verify hook permissions are 0o755 (standard)
    let hook_path = context.work_dir().join(".git/hooks/pre-commit");
    let metadata = fs_err::metadata(&hook_path).unwrap();
    let mode = metadata.permissions().mode() & 0o777;
    assert_eq!(
        mode, 0o755,
        "Hook should have standard permissions (0o755), got {mode:o}"
    );
}

/// Hook permissions should be group-writable when `core.sharedRepository` is set.
#[test]
#[cfg(unix)]
fn install_uses_group_permissions_for_shared_repository() {
    use std::os::unix::fs::PermissionsExt;

    let context = TestContext::new();
    context.init_project();

    // Set core.sharedRepository = group
    git_cmd(context.work_dir())
        .args(["config", "core.sharedRepository", "group"])
        .assert()
        .success();

    context.install().assert().success();

    // Verify hook permissions are 0o775 (group-writable)
    let hook_path = context.work_dir().join(".git/hooks/pre-commit");
    let metadata = fs_err::metadata(&hook_path).unwrap();
    let mode = metadata.permissions().mode() & 0o777;
    assert_eq!(
        mode, 0o775,
        "Hook should have group-writable permissions (0o775), got {mode:o}"
    );
}

/// Hook permissions should respect explicit octal `core.sharedRepository` values.
#[test]
#[cfg(unix)]
fn install_uses_explicit_shared_repository_mode() {
    use std::os::unix::fs::PermissionsExt;

    let context = TestContext::new();
    context.init_project();

    git_cmd(context.work_dir())
        .args(["config", "core.sharedRepository", "0640"])
        .assert()
        .success();

    context.install().assert().success();

    let hook_path = context.work_dir().join(".git/hooks/pre-commit");
    let metadata = fs_err::metadata(&hook_path).unwrap();
    let mode = metadata.permissions().mode() & 0o777;
    assert_eq!(
        mode, 0o750,
        "Hook should respect explicit shared mode (0o750), got {mode:o}"
    );
}

/// Run `prek install --prepare-hooks` to install the git hook and prepare prek hook environments.
#[test]
fn install_with_hooks() -> anyhow::Result<()> {
    let context = TestContext::new();
    context.init_project();
    context.write_pre_commit_config(indoc::indoc! {r"
        repos:
          - repo: https://github.com/pre-commit/pre-commit-hooks
            rev: v5.0.0
            hooks:
              - id: trailing-whitespace
          - repo: https://github.com/pre-commit/pre-commit-hooks
            rev: v5.0.0
            hooks:
              - id: trailing-whitespace
    "});

    context
        .home_dir()
        .child("repos")
        .assert(predicates::path::missing());
    context
        .home_dir()
        .child("hooks")
        .assert(predicates::path::missing());

    cmd_snapshot!(context.filters(), context.install().arg("--prepare-hooks"), @r#"
    success: true
    exit_code: 0
    ----- stdout -----
    prek installed at `.git/hooks/pre-commit`

    ----- stderr -----
    "#);

    // Check that repos and hooks are created.
    assert_eq!(context.home_dir().child("repos").read_dir()?.count(), 1);
    assert_eq!(context.home_dir().child("hooks").read_dir()?.count(), 1);

    insta::with_settings!(
        { filters => context.filters() },
        {
            assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
            #!/bin/sh
            # File generated by prek: https://github.com/j178/prek
            # ID: 182c10f181da4464a3eec51b83331688

            HERE="$(cd "$(dirname "$0")" && pwd)"
            PREK="[CURRENT_EXE]"

            # Check if the full path to prek is executable, otherwise fallback to PATH
            if [ ! -x "$PREK" ]; then
                PREK="prek"
            fi

            exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit -- "$@"
            "#);
        }
    );

    Ok(())
}

#[test]
fn install_with_legacy_install_hooks_flag_alias() -> anyhow::Result<()> {
    let context = TestContext::new();
    context.init_project();
    context.write_pre_commit_config(indoc::indoc! {r#"
        repos:
          - repo: local
            hooks:
              - id: test-hook
                name: Test Hook
                language: python
                entry: python -c 'print("test")'
    "#});

    context
        .home_dir()
        .child("hooks")
        .assert(predicates::path::missing());

    cmd_snapshot!(context.filters(), context.install().arg("--install-hooks"), @r#"
    success: true
    exit_code: 0
    ----- stdout -----
    prek installed at `.git/hooks/pre-commit`

    ----- stderr -----
    "#);

    assert_eq!(context.home_dir().child("hooks").read_dir()?.count(), 1);

    Ok(())
}

#[test]
fn install_with_existing_legacy_hook() -> anyhow::Result<()> {
    let context = TestContext::new();
    context.init_project();

    // Install our hook script first.
    context.install().assert().success();

    // Simulate an existing migrated legacy hook.
    context
        .work_dir()
        .child(".git/hooks/pre-commit.legacy")
        .write_str("#!/bin/sh\necho 'legacy'\n")?;

    // Without --overwrite, we should stay in migration mode.
    cmd_snapshot!(context.filters(), context.install(), @r"
    success: true
    exit_code: 0
    ----- stdout -----
    Migration mode: prek will also run legacy hook `.git/hooks/pre-commit.legacy`. Use `--overwrite` to remove legacy hooks.
    prek installed at `.git/hooks/pre-commit`

    ----- stderr -----
    ");
    context
        .work_dir()
        .child(".git/hooks/pre-commit.legacy")
        .assert(predicates::path::exists());

    // With --overwrite, the legacy script should be removed.
    cmd_snapshot!(context.filters(), context.install().arg("--overwrite"), @r#"
    success: true
    exit_code: 0
    ----- stdout -----
    Overwriting existing hook at `.git/hooks/pre-commit`
    prek installed at `.git/hooks/pre-commit`

    ----- stderr -----
    "#);
    context
        .work_dir()
        .child(".git/hooks/pre-commit.legacy")
        .assert(predicates::path::missing());

    Ok(())
}

/// Run `prek prepare-hooks` to prepare prek hook environments without installing the git hook.
#[test]
fn install_hooks_only() -> anyhow::Result<()> {
    let context = TestContext::new();
    context.init_project();
    context.write_pre_commit_config(indoc::indoc! {r"
        repos:
          - repo: https://github.com/pre-commit/pre-commit-hooks
            rev: v5.0.0
            hooks:
              - id: trailing-whitespace
          - repo: https://github.com/pre-commit/pre-commit-hooks
            rev: v5.0.0
            hooks:
              - id: trailing-whitespace
    "});

    context
        .home_dir()
        .child("repos")
        .assert(predicates::path::missing());
    context
        .home_dir()
        .child("hooks")
        .assert(predicates::path::missing());

    cmd_snapshot!(context.filters(), context.prepare_hooks(), @r#"
    success: true
    exit_code: 0
    ----- stdout -----

    ----- stderr -----
    "#);

    // Check that repos and hooks are created.
    assert_eq!(context.home_dir().child("repos").read_dir()?.count(), 1);
    assert_eq!(context.home_dir().child("hooks").read_dir()?.count(), 1);

    // Ensure the git hook is not installed.
    context
        .work_dir()
        .child(".git/hooks/pre-commit")
        .assert(predicates::path::missing());

    Ok(())
}

#[test]
fn install_with_legacy_install_hooks_subcommand_alias() -> anyhow::Result<()> {
    let context = TestContext::new();
    context.init_project();
    context.write_pre_commit_config(indoc::indoc! {r#"
        repos:
          - repo: local
            hooks:
              - id: test-hook
                name: Test Hook
                language: python
                entry: python -c 'print("test")'
        "#});

    context
        .home_dir()
        .child("hooks")
        .assert(predicates::path::missing());

    cmd_snapshot!(
        context.filters(),
        context.command().arg("install-hooks"),
        @r#"
    success: true
    exit_code: 0
    ----- stdout -----

    ----- stderr -----
    "#);

    assert_eq!(context.home_dir().child("hooks").read_dir()?.count(), 1);

    Ok(())
}

#[test]
fn uninstall() -> anyhow::Result<()> {
    let context = TestContext::new();

    context.init_project();

    // Hook does not exist.
    cmd_snapshot!(context.filters(), context.uninstall(), @r#"
    success: true
    exit_code: 0
    ----- stdout -----

    ----- stderr -----
    `.git/hooks/pre-commit` does not exist, skipping.
    "#);

    // Uninstall `pre-commit` hook.
    context.install().assert().success();
    cmd_snapshot!(context.filters(), context.uninstall(), @r#"
    success: true
    exit_code: 0
    ----- stdout -----
    Uninstalled `pre-commit`

    ----- stderr -----
    "#);
    context
        .work_dir()
        .child(".git/hooks/pre-commit")
        .assert(predicates::path::missing());

    // Hook is not managed by `pre-commit`.
    context
        .work_dir()
        .child(".git/hooks/pre-commit")
        .write_str("#!/bin/sh\necho 'pre-commit'\n")?;
    cmd_snapshot!(context.filters(), context.uninstall(), @r#"
    success: true
    exit_code: 0
    ----- stdout -----

    ----- stderr -----
    `.git/hooks/pre-commit` is not managed by prek, skipping.
    "#);

    // Restore previous hook.
    context.install().assert().success();
    cmd_snapshot!(context.filters(), context.uninstall(), @"
    success: true
    exit_code: 0
    ----- stdout -----
    Uninstalled `pre-commit`
    Restored `.git/hooks/pre-commit.legacy` to `.git/hooks/pre-commit`

    ----- stderr -----
    ");

    // Uninstall multiple hooks.
    context
        .install()
        .arg("-t")
        .arg("pre-commit")
        .arg("-t")
        .arg("post-commit")
        .assert()
        .success();
    cmd_snapshot!(context.filters(), context.uninstall().arg("-t").arg("pre-commit").arg("-t").arg("post-commit"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    Uninstalled `pre-commit`
    Restored `.git/hooks/pre-commit.legacy` to `.git/hooks/pre-commit`
    Uninstalled `post-commit`

    ----- stderr -----
    ");

    Ok(())
}

#[test]
fn uninstall_with_local_hooks_path_removes_configured_hook() {
    let context = TestContext::new();
    context.init_project();

    git_cmd(context.work_dir())
        .args(["config", "core.hooksPath", "custom-hooks"])
        .assert()
        .success();
    context.install().assert().success();

    cmd_snapshot!(context.filters(), context.uninstall(), @r#"
    success: true
    exit_code: 0
    ----- stdout -----
    Uninstalled `pre-commit`

    ----- stderr -----
    "#);

    context
        .work_dir()
        .child("custom-hooks/pre-commit")
        .assert(predicates::path::missing());
}

#[test]
fn uninstall_with_worktree_hooks_path_removes_configured_hook() -> anyhow::Result<()> {
    let context = TestContext::new();
    context.init_project();
    context.work_dir().child("README.md").write_str("hello\n")?;
    context.git_add(".");
    context.git_commit("Initial commit");

    git_cmd(context.work_dir())
        .args(["config", "extensions.worktreeConfig", "true"])
        .assert()
        .success();
    git_cmd(context.work_dir())
        .args(["worktree", "add", "worktree", "HEAD"])
        .assert()
        .success();

    let worktree = context.work_dir().child("worktree");
    let output = git_cmd(&worktree)
        .args(["rev-parse", "--path-format=absolute", "--git-dir"])
        .output()?;
    let worktree_git_dir = String::from_utf8(output.stdout)?.trim().to_string();
    let worktree_hooks = std::path::PathBuf::from(&worktree_git_dir).join("hooks");
    let worktree_hooks_str = worktree_hooks.display().to_string();

    git_cmd(&worktree)
        .args([
            "config",
            "--worktree",
            "core.hooksPath",
            &worktree_hooks_str,
        ])
        .assert()
        .success();

    context.install().assert().success();
    assert!(
        context.work_dir().join(".git/hooks/pre-commit").exists(),
        "expected hook to remain installed in the main worktree",
    );

    let mut install = context.install();
    install.current_dir(&worktree);
    install.assert().success();
    assert!(
        worktree_hooks.join("pre-commit").exists(),
        "expected hook to be installed into {}",
        worktree_hooks.display()
    );

    let mut uninstall = context.uninstall();
    uninstall.current_dir(&worktree);
    cmd_snapshot!(context.filters(), uninstall, @r#"
    success: true
    exit_code: 0
    ----- stdout -----
    Uninstalled `pre-commit`

    ----- stderr -----
    "#);

    assert!(
        !worktree_hooks.join("pre-commit").exists(),
        "expected hook to be removed from {}",
        worktree_hooks.display()
    );
    context
        .work_dir()
        .child(".git/hooks/pre-commit")
        .assert(predicates::path::exists());

    Ok(())
}

#[test]
fn uninstall_refuses_external_hooks_path_set() {
    let context = TestContext::new();
    context.init_project();

    let global_gitconfig = context.work_dir().join("global.gitconfig");
    git_cmd(context.work_dir())
        .env("GIT_CONFIG_GLOBAL", &global_gitconfig)
        .args(["config", "--global", "core.hooksPath", "custom-hooks"])
        .assert()
        .success();

    let mut uninstall = context.uninstall();
    uninstall.env("GIT_CONFIG_GLOBAL", &global_gitconfig);
    cmd_snapshot!(context.filters(), uninstall, @"
    success: false
    exit_code: 2
    ----- stdout -----

    ----- stderr -----
    error: Refusing to uninstall hooks because `core.hooksPath` is configured outside this repository.

    note: Git will execute hooks from the configured global/system hooks directory, not from this repository's hooks directory.

    hint: Remove the global/system setting, or move `core.hooksPath` into repo scope for this repository instead.
      git config --unset-all --global core.hooksPath
      git config --unset-all --system core.hooksPath
      git config --local core.hooksPath <path>
    ");
}

#[test]
fn uninstall_with_git_dir_allows_external_hooks_path_set() {
    let context = TestContext::new();
    context.init_project();

    let global_gitconfig = context.work_dir().join("global.gitconfig");
    git_cmd(context.work_dir())
        .env("GIT_CONFIG_GLOBAL", &global_gitconfig)
        .args(["config", "--global", "core.hooksPath", "custom-hooks"])
        .assert()
        .success();

    let mut install = context.install();
    install
        .arg("--git-dir")
        .arg(".git")
        .env("GIT_CONFIG_GLOBAL", &global_gitconfig);
    install.assert().success();

    let mut uninstall = context.uninstall();
    uninstall
        .arg("--git-dir")
        .arg(".git")
        .env("GIT_CONFIG_GLOBAL", &global_gitconfig);
    cmd_snapshot!(context.filters(), uninstall, @r#"
    success: true
    exit_code: 0
    ----- stdout -----
    Uninstalled `pre-commit`

    ----- stderr -----
    "#);

    context
        .work_dir()
        .child(".git/hooks/pre-commit")
        .assert(predicates::path::missing());
}

#[test]
fn uninstall_refuses_empty_external_hooks_path_set() {
    let context = TestContext::new();
    context.init_project();

    let global_gitconfig = context.work_dir().join("global.gitconfig");
    git_cmd(context.work_dir())
        .env("GIT_CONFIG_GLOBAL", &global_gitconfig)
        .args(["config", "--global", "core.hooksPath", ""])
        .assert()
        .success();

    let mut uninstall = context.uninstall();
    uninstall.env("GIT_CONFIG_GLOBAL", &global_gitconfig);
    cmd_snapshot!(context.filters(), uninstall, @"
    success: false
    exit_code: 2
    ----- stdout -----

    ----- stderr -----
    error: Refusing to uninstall hooks because `core.hooksPath` is configured outside this repository.

    note: Git will execute hooks from the configured global/system hooks directory, not from this repository's hooks directory.

    hint: Remove the global/system setting, or move `core.hooksPath` into repo scope for this repository instead.
      git config --unset-all --global core.hooksPath
      git config --unset-all --system core.hooksPath
      git config --local core.hooksPath <path>
    ");
}

#[test]
fn uninstall_refuses_empty_local_hooks_path_set() {
    let context = TestContext::new();
    context.init_project();

    git_cmd(context.work_dir())
        .args(["config", "core.hooksPath", ""])
        .assert()
        .success();

    cmd_snapshot!(context.filters(), context.uninstall(), @r#"
    success: false
    exit_code: 2
    ----- stdout -----

    ----- stderr -----
    error: Git resolved hooks directory to the current directory (`.`). Unset `core.hooksPath` or set it to a real directory path.
    "#);
}

#[test]
fn uninstall_with_dot_hooks_path_removes_root_hook() {
    let context = TestContext::new();
    context.init_project();

    git_cmd(context.work_dir())
        .args(["config", "core.hooksPath", "."])
        .assert()
        .success();

    context.install().assert().success();
    context.uninstall().assert().success();

    context
        .work_dir()
        .child("pre-commit")
        .assert(predicates::path::missing());
}

#[test]
fn uninstall_with_included_local_hooks_path_removes_configured_hook() -> anyhow::Result<()> {
    let context = TestContext::new();
    context.init_project();

    context
        .work_dir()
        .child("included-hooks.cfg")
        .write_str(indoc! {r"
        [core]
            hooksPath = custom-hooks
    "})?;

    git_cmd(context.work_dir())
        .args(["config", "--local", "include.path", "../included-hooks.cfg"])
        .assert()
        .success();

    context.install().assert().success();
    context.uninstall().assert().success();

    context
        .work_dir()
        .child("custom-hooks/pre-commit")
        .assert(predicates::path::missing());

    Ok(())
}

/// `prek uninstall --all` should remove all prek-managed hooks.
#[test]
fn uninstall_all_managed_hooks() -> anyhow::Result<()> {
    let context = TestContext::new();
    context.init_project();

    // Install both pre-commit and pre-push hooks.
    context
        .install()
        .arg("-t")
        .arg("pre-commit")
        .arg("-t")
        .arg("pre-push")
        .assert()
        .success();
    assert!(context.work_dir().join(".git/hooks/pre-commit").exists());
    assert!(context.work_dir().join(".git/hooks/pre-push").exists());

    let custom_hook = "#!/bin/sh\necho 'custom pre-commit'\n";
    context
        .work_dir()
        .child(".git/hooks/pre-commit")
        .write_str(custom_hook)?;

    // Uninstall with `--all` should only remove managed hooks.
    cmd_snapshot!(context.filters(), context.uninstall().arg("--all"), @r"
    success: true
    exit_code: 0
    ----- stdout -----
    Uninstalled `pre-push`

    ----- stderr -----
    ");

    assert_eq!(context.read(".git/hooks/pre-commit"), custom_hook);
    assert!(!context.work_dir().join(".git/hooks/pre-push").exists());

    Ok(())
}

#[test]
fn uninstall_remove_legacy_hook() -> anyhow::Result<()> {
    let context = TestContext::new();
    context.init_project();

    // Create the `pre-commit` hook.
    context.install().assert().success();
    // Create the `pre-commit.legacy` file.
    fs_err::copy(
        context.work_dir().join(".git/hooks/pre-commit"),
        context.work_dir().join(".git/hooks/pre-commit.legacy"),
    )?;

    // Uninstall should remove the `pre-commit.legacy` file too.
    cmd_snapshot!(context.filters(), context.uninstall(), @"
    success: true
    exit_code: 0
    ----- stdout -----
    Uninstalled `pre-commit`

    ----- stderr -----
    Found legacy hook at `.git/hooks/pre-commit.legacy`, removing it.
    ");
    context
        .work_dir()
        .child(".git/hooks/pre-commit")
        .assert(predicates::path::missing());
    context
        .work_dir()
        .child(".git/hooks/pre-commit.legacy")
        .assert(predicates::path::missing());

    // Create a legacy script that is not ours and ensure it is not removed.
    context.install().assert().success();
    context
        .work_dir()
        .child(".git/hooks/pre-commit.legacy")
        .write_str("#!/bin/sh\necho 'legacy'\n")?;

    cmd_snapshot!(context.filters(), context.uninstall(), @"
    success: true
    exit_code: 0
    ----- stdout -----
    Uninstalled `pre-commit`
    Restored `.git/hooks/pre-commit.legacy` to `.git/hooks/pre-commit`

    ----- stderr -----
    ");
    context
        .work_dir()
        .child(".git/hooks/pre-commit")
        .assert(predicates::path::exists());
    context
        .work_dir()
        .child(".git/hooks/pre-commit.legacy")
        .assert(predicates::path::missing());

    Ok(())
}

#[test]
fn init_template_dir() -> anyhow::Result<()> {
    let context = TestContext::new();
    context.init_project();

    cmd_snapshot!(context.filters(), context.command().arg("init-templatedir").arg(".git"), @r#"
    success: true
    exit_code: 0
    ----- stdout -----
    prek installed at `.git/hooks/pre-commit`

    ----- stderr -----
    warning: git config `init.templateDir` not set to the target directory, try `git config --global init.templateDir '.git'`
    "#);

    insta::with_settings!(
        { filters => context.filters() },
        {
            assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
            #!/bin/sh
            # File generated by prek: https://github.com/j178/prek
            # ID: 182c10f181da4464a3eec51b83331688

            HERE="$(cd "$(dirname "$0")" && pwd)"
            PREK="[CURRENT_EXE]"

            # Check if the full path to prek is executable, otherwise fallback to PATH
            if [ ! -x "$PREK" ]; then
                PREK="prek"
            fi

            exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit --skip-on-missing-config -- "$@"
            "#);
        }
    );

    // Run from a subdirectory.
    let child = context.work_dir().child("subdir");
    child.create_dir_all()?;

    cmd_snapshot!(context.filters(), context.command().arg("init-templatedir").arg("temp-dir").current_dir(child), @r#"
    success: true
    exit_code: 0
    ----- stdout -----
    prek installed at `temp-dir/hooks/pre-commit`

    ----- stderr -----
    warning: git config `init.templateDir` not set to the target directory, try `git config --global init.templateDir 'temp-dir'`
    "#);
    insta::with_settings!(
        { filters => context.filters() },
        {
            assert_snapshot!(context.read("subdir/temp-dir/hooks/pre-commit"), @r#"
            #!/bin/sh
            # File generated by prek: https://github.com/j178/prek
            # ID: 182c10f181da4464a3eec51b83331688

            HERE="$(cd "$(dirname "$0")" && pwd)"
            PREK="[CURRENT_EXE]"

            # Check if the full path to prek is executable, otherwise fallback to PATH
            if [ ! -x "$PREK" ]; then
                PREK="prek"
            fi

            exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit --skip-on-missing-config -- "$@"
            "#);
        }
    );

    // `--config` points to non-existing file.
    cmd_snapshot!(context.filters(), context.command().arg("init-templatedir").arg("-c").arg("non-exist-config").arg("subdir2"), @r"
    success: true
    exit_code: 0
    ----- stdout -----
    prek installed at `subdir2/hooks/pre-commit` with specified config `non-exist-config`

    ----- stderr -----
    warning: git config `init.templateDir` not set to the target directory, try `git config --global init.templateDir 'subdir2'`
    ");
    insta::with_settings!(
        { filters => context.filters() },
        {
            assert_snapshot!(context.read("subdir2/hooks/pre-commit"), @r#"
            #!/bin/sh
            # File generated by prek: https://github.com/j178/prek
            # ID: 182c10f181da4464a3eec51b83331688

            HERE="$(cd "$(dirname "$0")" && pwd)"
            PREK="[CURRENT_EXE]"

            # Check if the full path to prek is executable, otherwise fallback to PATH
            if [ ! -x "$PREK" ]; then
                PREK="prek"
            fi

            exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit --config="non-exist-config" --skip-on-missing-config -- "$@"
            "#);
        }
    );

    Ok(())
}

/// Tests `prek util init-template-dir` works.
#[test]
fn util_init_template_dir() {
    let context = TestContext::new();
    context.init_project();

    cmd_snapshot!(context.filters(), context.command().arg("util").arg("init-templatedir").arg(".git"), @r#"
    success: true
    exit_code: 0
    ----- stdout -----
    prek installed at `.git/hooks/pre-commit`

    ----- stderr -----
    warning: git config `init.templateDir` not set to the target directory, try `git config --global init.templateDir '.git'`
    "#);

    insta::with_settings!(
        { filters => context.filters() },
        {
            assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
            #!/bin/sh
            # File generated by prek: https://github.com/j178/prek
            # ID: 182c10f181da4464a3eec51b83331688

            HERE="$(cd "$(dirname "$0")" && pwd)"
            PREK="[CURRENT_EXE]"

            # Check if the full path to prek is executable, otherwise fallback to PATH
            if [ ! -x "$PREK" ]; then
                PREK="prek"
            fi

            exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit --skip-on-missing-config -- "$@"
            "#);
        }
    );
}

/// Tests `prek init-template-dir` in a non-git repository.
#[test]
fn init_template_dir_non_git_repo() {
    let context = TestContext::new();

    cmd_snapshot!(context.filters(), context.command().arg("init-template-dir").arg(".git"), @r#"
    success: true
    exit_code: 0
    ----- stdout -----
    prek installed at `.git/hooks/pre-commit`

    ----- stderr -----
    warning: git config `init.templateDir` not set to the target directory, try `git config --global init.templateDir '.git'`
    "#);

    context.write_pre_commit_config(indoc::indoc! {"
        default_install_hook_types:
          - pre-commit
          - commit-msg
          - pre-push
        repos:
    "});
    cmd_snapshot!(context.filters(), context.command().arg("init-template-dir").arg("-c").arg(context.work_dir().join(PRE_COMMIT_CONFIG_YAML)).arg(".git"), @r"
    success: true
    exit_code: 0
    ----- stdout -----
    Overwriting existing hook at `.git/hooks/pre-commit`
    prek installed at `.git/hooks/pre-commit` with specified config `[TEMP_DIR]/.pre-commit-config.yaml`
    prek installed at `.git/hooks/commit-msg` with specified config `[TEMP_DIR]/.pre-commit-config.yaml`
    prek installed at `.git/hooks/pre-push` with specified config `[TEMP_DIR]/.pre-commit-config.yaml`

    ----- stderr -----
    warning: git config `init.templateDir` not set to the target directory, try `git config --global init.templateDir '.git'`
    ");
}

#[test]
fn workspace_install() -> anyhow::Result<()> {
    let context = TestContext::new();
    context.init_project();

    let config = indoc! {r#"
    repos:
      - repo: local
        hooks:
        - id: test-hook
          name: Test Hook
          language: python
          entry: python -c 'print("test")'
    "#};

    context.setup_workspace(
        &[
            "project2",
            "project3",
            "nested/project4",
            "project3/project5",
        ],
        config,
    )?;
    context.git_add(".");

    // Install from root directory.
    cmd_snapshot!(context.filters(), context.install(), @r"
    success: true
    exit_code: 0
    ----- stdout -----
    prek installed at `.git/hooks/pre-commit`

    ----- stderr -----
    ");

    insta::with_settings!(
        { filters => context.filters() },
        {
            assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
            #!/bin/sh
            # File generated by prek: https://github.com/j178/prek
            # ID: 182c10f181da4464a3eec51b83331688

            HERE="$(cd "$(dirname "$0")" && pwd)"
            PREK="[CURRENT_EXE]"

            # Check if the full path to prek is executable, otherwise fallback to PATH
            if [ ! -x "$PREK" ]; then
                PREK="prek"
            fi

            exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit -- "$@"
            "#);
        }
    );

    // Install from a subdirectory.
    cmd_snapshot!(context.filters(), context.install().current_dir(context.work_dir().join("project3")), @r"
    success: true
    exit_code: 0
    ----- stdout -----
    prek installed at `../.git/hooks/pre-commit` for workspace `[TEMP_DIR]/project3`

    hint: this hook installed for `[TEMP_DIR]/project3` only; run `prek install` from `[TEMP_DIR]/` to install for the entire repo.

    ----- stderr -----
    ");

    insta::with_settings!(
        { filters => context.filters() },
        {
            assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
            #!/bin/sh
            # File generated by prek: https://github.com/j178/prek
            # ID: 182c10f181da4464a3eec51b83331688

            HERE="$(cd "$(dirname "$0")" && pwd)"
            PREK="[CURRENT_EXE]"

            # Check if the full path to prek is executable, otherwise fallback to PATH
            if [ ! -x "$PREK" ]; then
                PREK="prek"
            fi

            exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit --cd="project3" -- "$@"
            "#);
        }
    );

    // Install with selectors
    cmd_snapshot!(context.filters(), context.install().arg("project3/").arg("--skip").arg("project2/"), @r"
    success: true
    exit_code: 0
    ----- stdout -----
    prek installed at `.git/hooks/pre-commit`

    ----- stderr -----
    ");

    insta::with_settings!(
        { filters => context.filters() },
        {
            assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
            #!/bin/sh
            # File generated by prek: https://github.com/j178/prek
            # ID: 182c10f181da4464a3eec51b83331688

            HERE="$(cd "$(dirname "$0")" && pwd)"
            PREK="[CURRENT_EXE]"

            # Check if the full path to prek is executable, otherwise fallback to PATH
            if [ ! -x "$PREK" ]; then
                PREK="prek"
            fi

            exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 project3/ --skip=project2/ --hook-type=pre-commit -- "$@"
            "#);
        }
    );

    // Invalid selectors
    cmd_snapshot!(context.filters(), context.install().arg(":"), @r"
    success: false
    exit_code: 2
    ----- stdout -----

    ----- stderr -----
    error: Invalid selector: `:`
      caused by: hook ID part is empty
    ");

    // SKIP env var is ignored
    cmd_snapshot!(context.filters(), context.install().arg("project3/").env(EnvVars::SKIP, "project5/"), @r"
    success: true
    exit_code: 0
    ----- stdout -----
    prek installed at `.git/hooks/pre-commit`

    ----- stderr -----
    warning: Skip selectors from environment variables `SKIP` are ignored during installing hooks.
    ");

    insta::with_settings!(
        { filters => context.filters() },
        {
            assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
            #!/bin/sh
            # File generated by prek: https://github.com/j178/prek
            # ID: 182c10f181da4464a3eec51b83331688

            HERE="$(cd "$(dirname "$0")" && pwd)"
            PREK="[CURRENT_EXE]"

            # Check if the full path to prek is executable, otherwise fallback to PATH
            if [ ! -x "$PREK" ]; then
                PREK="prek"
            fi

            exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 project3/ --hook-type=pre-commit -- "$@"
            "#);
        }
    );

    Ok(())
}

#[test]
fn workspace_install_hooks() -> anyhow::Result<()> {
    let context = TestContext::new();
    context.init_project();

    let config = indoc! {r#"
    repos:
      - repo: local
        hooks:
        - id: test-hook
          name: Test Hook
          language: python
          entry: python -c 'print("test")'
    "#};

    context.setup_workspace(
        &[
            "project2",
            "project3",
            "nested/project4",
            "project3/project5",
        ],
        config,
    )?;
    context.git_add(".");

    // Install by selectors
    cmd_snapshot!(context.filters(), context.prepare_hooks().arg("project3").arg("--skip").arg("project3/project5/"), @r"
    success: true
    exit_code: 0
    ----- stdout -----

    ----- stderr -----
    ");

    // Install all hooks
    cmd_snapshot!(context.filters(), context.prepare_hooks(), @r"
    success: true
    exit_code: 0
    ----- stdout -----

    ----- stderr -----
    ");

    // Check that hooks are created.
    assert_eq!(context.home_dir().child("hooks").read_dir()?.count(), 1);

    Ok(())
}

/// Only install root config's hook types in a workspace.
#[test]
fn workspace_install_only_root_hook_types() -> anyhow::Result<()> {
    let context = TestContext::new();
    context.init_project();

    let root_config = indoc! {r#"
    default_install_hook_types: [pre-commit, post-commit]
    repos:
      - repo: local
        hooks:
        - id: root-hook
          name: Root Hook
          language: python
          entry: python -c 'print("root")'
    "#};

    let nested_config = indoc! {r#"
    default_install_hook_types: [pre-push, post-merge]
    repos:
      - repo: local
        hooks:
        - id: nested-hook
          name: Nested Hook
          language: python
          entry: python -c 'print("nested")'
    "#};

    context
        .work_dir()
        .child(PRE_COMMIT_CONFIG_YAML)
        .write_str(root_config)?;
    context.work_dir().child("project2").create_dir_all()?;
    context
        .work_dir()
        .child("project2")
        .child(PRE_COMMIT_CONFIG_YAML)
        .write_str(nested_config)?;
    context.git_add(".");

    cmd_snapshot!(context.filters(), context.install(), @r"
    success: true
    exit_code: 0
    ----- stdout -----
    prek installed at `.git/hooks/pre-commit`
    prek installed at `.git/hooks/post-commit`

    ----- stderr -----
    ");

    // Should only install pre-commit and post-commit hooks from root config
    assert!(context.work_dir().join(".git/hooks/pre-commit").exists());
    assert!(context.work_dir().join(".git/hooks/post-commit").exists());
    assert!(!context.work_dir().join(".git/hooks/pre-push").exists());
    assert!(!context.work_dir().join(".git/hooks/post-merge").exists());

    Ok(())
}

#[test]
fn workspace_uninstall() -> anyhow::Result<()> {
    let context = TestContext::new();
    context.init_project();

    let config = indoc! {r#"
    repos:
      - repo: local
        hooks:
        - id: test-hook
          name: Test Hook
          language: python
          entry: python -c 'print("test")'
    "#};

    context.setup_workspace(
        &[
            "project2",
            "project3",
            "nested/project4",
            "project3/project5",
        ],
        config,
    )?;
    context.git_add(".");

    // Install first
    context.install().assert().success();

    // Then uninstall
    cmd_snapshot!(context.filters(), context.uninstall(), @r"
    success: true
    exit_code: 0
    ----- stdout -----
    Uninstalled `pre-commit`

    ----- stderr -----
    ");

    // Verify hooks are removed
    assert!(!context.work_dir().join(".git/hooks/pre-commit").exists());

    Ok(())
}

#[test]
fn workspace_init_template_dir() -> anyhow::Result<()> {
    let context = TestContext::new();
    context.init_project();

    let config = indoc! {r#"
    repos:
      - repo: local
        hooks:
        - id: test-hook
          name: Test Hook
          language: python
          entry: python -c "print('test')"
    "#};

    context.setup_workspace(
        &[
            "project2",
            "project3",
            "nested/project4",
            "project3/project5",
        ],
        config,
    )?;
    context.git_add(".");

    // Create a template directory
    let template_dir = context.work_dir().child("template");
    template_dir.create_dir_all()?;

    cmd_snapshot!(context.filters(), context.command().arg("init-template-dir").arg(&*template_dir), @r"
    success: true
    exit_code: 0
    ----- stdout -----
    prek installed at `template/hooks/pre-commit`

    ----- stderr -----
    warning: git config `init.templateDir` not set to the target directory, try `git config --global init.templateDir '[TEMP_DIR]/template'`
    ");

    // Check that hooks are created in the template directory
    assert!(template_dir.join("hooks/pre-commit").exists());

    let filters = context.filters();
    insta::with_settings!(
        { filters => filters.clone() },
        {
            insta::assert_snapshot!(context.read("template/hooks/pre-commit"), @r#"
            #!/bin/sh
            # File generated by prek: https://github.com/j178/prek
            # ID: 182c10f181da4464a3eec51b83331688

            HERE="$(cd "$(dirname "$0")" && pwd)"
            PREK="[CURRENT_EXE]"

            # Check if the full path to prek is executable, otherwise fallback to PATH
            if [ ! -x "$PREK" ]; then
                PREK="prek"
            fi

            exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit --skip-on-missing-config -- "$@"
            "#);
        }
    );

    Ok(())
}

/// Test that a warning is shown when the config file exists but is invalid.
#[test]
fn install_invalid_config_warning() {
    let context = TestContext::new();
    context.init_project();

    // Write an invalid config (missing required `rev` field).
    context.write_pre_commit_config(indoc! {r"
        repos:
          - repo: https://github.com/pre-commit/pre-commit-hooks
            hooks:
              - id: trailing-whitespace
    "});

    // Install should succeed but show a warning about the invalid config.
    cmd_snapshot!(context.filters(), context.install(), @"
    success: true
    exit_code: 0
    ----- stdout -----
    prek installed at `.git/hooks/pre-commit`

    ----- stderr -----
    warning: Failed to parse `.pre-commit-config.yaml`: error: line 3 column 5: missing field `rev`
     --> <input>:3:5
      |
    1 | repos:
    2 |   - repo: https://github.com/pre-commit/pre-commit-hooks
    3 |     hooks:
      |     ^ missing field `rev`
    4 |       - id: trailing-whitespace
      |
    ");
}
