use crate::common::{TestContext, cmd_snapshot};
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::CONFIG_FILE;
use prek_consts::env_vars::EnvVars;

mod common;

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

    let filters = context
        .filters()
        .into_iter()
        .chain([("#!/bin/sh", "#!/usr/bin/env bash")])
        .collect::<Vec<_>>();

    // 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 => filters.clone() },
        {
            assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
            #!/usr/bin/env bash
            # File generated by prek: https://github.com/j178/prek
            # ID: 182c10f181da4464a3eec51b83331688

            # Unset GIT_DIR to avoid git taking current directory as the git dir.
            # See:
            # https://github.com/pre-commit/pre-commit/issues/2295
            # https://www.spinics.net/lists/git/msg374197.html
            unset GIT_DIR

            ARGS=(hook-impl --hook-type=pre-commit --script-version=3)

            HERE="$(cd "$(dirname "$0")" && pwd)"
            ARGS+=(--hook-dir "$HERE" -- "$@")
            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" "${ARGS[@]}"
            "#);
        }
    );

    // 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!(filters.clone(), 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`
    prek installed at `.git/hooks/pre-commit`
    prek installed at `.git/hooks/post-commit`

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

            # Unset GIT_DIR to avoid git taking current directory as the git dir.
            # See:
            # https://github.com/pre-commit/pre-commit/issues/2295
            # https://www.spinics.net/lists/git/msg374197.html
            unset GIT_DIR

            ARGS=(hook-impl --hook-type=pre-commit --script-version=3)

            HERE="$(cd "$(dirname "$0")" && pwd)"
            ARGS+=(--hook-dir "$HERE" -- "$@")
            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" "${ARGS[@]}"
            "#);
        }
    );

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

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

            # Unset GIT_DIR to avoid git taking current directory as the git dir.
            # See:
            # https://github.com/pre-commit/pre-commit/issues/2295
            # https://www.spinics.net/lists/git/msg374197.html
            unset GIT_DIR

            ARGS=(hook-impl --hook-type=post-commit --script-version=3)

            HERE="$(cd "$(dirname "$0")" && pwd)"
            ARGS+=(--hook-dir "$HERE" -- "$@")
            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" "${ARGS[@]}"
            "#);
        }
    );

    // Overwrite existing hooks.
    cmd_snapshot!(filters.clone(), 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 => filters.clone() },
        {
            assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
            #!/usr/bin/env bash
            # File generated by prek: https://github.com/j178/prek
            # ID: 182c10f181da4464a3eec51b83331688

            # Unset GIT_DIR to avoid git taking current directory as the git dir.
            # See:
            # https://github.com/pre-commit/pre-commit/issues/2295
            # https://www.spinics.net/lists/git/msg374197.html
            unset GIT_DIR

            ARGS=(hook-impl --hook-type=pre-commit --script-version=3)

            HERE="$(cd "$(dirname "$0")" && pwd)"
            ARGS+=(--hook-dir "$HERE" -- "$@")
            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" "${ARGS[@]}"
            "#);
        }
    );
    insta::with_settings!(
        { filters => filters.clone() },
        {
            assert_snapshot!(context.read(".git/hooks/post-commit"), @r#"
            #!/usr/bin/env bash
            # File generated by prek: https://github.com/j178/prek
            # ID: 182c10f181da4464a3eec51b83331688

            # Unset GIT_DIR to avoid git taking current directory as the git dir.
            # See:
            # https://github.com/pre-commit/pre-commit/issues/2295
            # https://www.spinics.net/lists/git/msg374197.html
            unset GIT_DIR

            ARGS=(hook-impl --hook-type=post-commit --script-version=3)

            HERE="$(cd "$(dirname "$0")" && pwd)"
            ARGS+=(--hook-dir "$HERE" -- "$@")
            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" "${ARGS[@]}"
            "#);
        }
    );

    Ok(())
}

/// Run `prek install --install-hooks` to install the git hook and create 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
    "});

    let filters = context
        .filters()
        .into_iter()
        .chain([("#!/bin/sh", "#!/usr/bin/env bash")])
        .collect::<Vec<_>>();

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

    cmd_snapshot!(filters.clone(), context.install().arg("--install-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 => filters },
        {
            assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
            #!/usr/bin/env bash
            # File generated by prek: https://github.com/j178/prek
            # ID: 182c10f181da4464a3eec51b83331688

            # Unset GIT_DIR to avoid git taking current directory as the git dir.
            # See:
            # https://github.com/pre-commit/pre-commit/issues/2295
            # https://www.spinics.net/lists/git/msg374197.html
            unset GIT_DIR

            ARGS=(hook-impl --hook-type=pre-commit --script-version=3)

            HERE="$(cd "$(dirname "$0")" && pwd)"
            ARGS+=(--hook-dir "$HERE" -- "$@")
            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" "${ARGS[@]}"
            "#);
        }
    );

    Ok(())
}

/// Run `prek install-hooks` to create 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.install_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 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(), @r#"
    success: true
    exit_code: 0
    ----- stdout -----
    Uninstalled `pre-commit`
    Restored previous hook 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"), @r#"
    success: true
    exit_code: 0
    ----- stdout -----
    Uninstalled `pre-commit`
    Restored previous hook to `.git/hooks/pre-commit`
    Uninstalled `post-commit`

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

    Ok(())
}

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

    let filters = context
        .filters()
        .into_iter()
        .chain([("#!/bin/sh", "#!/usr/bin/env bash")])
        .collect::<Vec<_>>();

    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 => filters.clone() },
        {
            assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
            #!/usr/bin/env bash
            # File generated by prek: https://github.com/j178/prek
            # ID: 182c10f181da4464a3eec51b83331688

            # Unset GIT_DIR to avoid git taking current directory as the git dir.
            # See:
            # https://github.com/pre-commit/pre-commit/issues/2295
            # https://www.spinics.net/lists/git/msg374197.html
            unset GIT_DIR

            ARGS=(hook-impl --hook-type=pre-commit --skip-on-missing-config --script-version=3)

            HERE="$(cd "$(dirname "$0")" && pwd)"
            ARGS+=(--hook-dir "$HERE" -- "$@")
            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" "${ARGS[@]}"
            "#);
        }
    );

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

    cmd_snapshot!(filters.clone(), 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 => filters.clone() },
        {
            assert_snapshot!(context.read("subdir/temp-dir/hooks/pre-commit"), @r#"
            #!/usr/bin/env bash
            # File generated by prek: https://github.com/j178/prek
            # ID: 182c10f181da4464a3eec51b83331688

            # Unset GIT_DIR to avoid git taking current directory as the git dir.
            # See:
            # https://github.com/pre-commit/pre-commit/issues/2295
            # https://www.spinics.net/lists/git/msg374197.html
            unset GIT_DIR

            ARGS=(hook-impl --hook-type=pre-commit --skip-on-missing-config --script-version=3)

            HERE="$(cd "$(dirname "$0")" && pwd)"
            ARGS+=(--hook-dir "$HERE" -- "$@")
            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" "${ARGS[@]}"
            "#);
        }
    );

    // `--config` points to non-existing file.
    cmd_snapshot!(filters.clone(), 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 => filters.clone() },
        {
            assert_snapshot!(context.read("subdir2/hooks/pre-commit"), @r#"
            #!/usr/bin/env bash
            # File generated by prek: https://github.com/j178/prek
            # ID: 182c10f181da4464a3eec51b83331688

            # Unset GIT_DIR to avoid git taking current directory as the git dir.
            # See:
            # https://github.com/pre-commit/pre-commit/issues/2295
            # https://www.spinics.net/lists/git/msg374197.html
            unset GIT_DIR

            ARGS=(hook-impl --hook-type=pre-commit --config="non-exist-config" --skip-on-missing-config --script-version=3)

            HERE="$(cd "$(dirname "$0")" && pwd)"
            ARGS+=(--hook-dir "$HERE" -- "$@")
            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" "${ARGS[@]}"
            "#);
        }
    );

    Ok(())
}

#[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 -----
    ");

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

            # Unset GIT_DIR to avoid git taking current directory as the git dir.
            # See:
            # https://github.com/pre-commit/pre-commit/issues/2295
            # https://www.spinics.net/lists/git/msg374197.html
            unset GIT_DIR

            ARGS=(hook-impl --hook-type=pre-commit --script-version=3)

            HERE="$(cd "$(dirname "$0")" && pwd)"
            ARGS+=(--hook-dir "$HERE" -- "$@")
            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" "${ARGS[@]}"
            "#);
        }
    );

    // 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 -----
    ");

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

            # Unset GIT_DIR to avoid git taking current directory as the git dir.
            # See:
            # https://github.com/pre-commit/pre-commit/issues/2295
            # https://www.spinics.net/lists/git/msg374197.html
            unset GIT_DIR

            ARGS=(hook-impl --hook-type=pre-commit --cd="project3" --script-version=3)

            HERE="$(cd "$(dirname "$0")" && pwd)"
            ARGS+=(--hook-dir "$HERE" -- "$@")
            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" "${ARGS[@]}"
            "#);
        }
    );

    // 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 => filters.clone() },
        {
            assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
            #!/usr/bin/env bash
            # File generated by prek: https://github.com/j178/prek
            # ID: 182c10f181da4464a3eec51b83331688

            # Unset GIT_DIR to avoid git taking current directory as the git dir.
            # See:
            # https://github.com/pre-commit/pre-commit/issues/2295
            # https://www.spinics.net/lists/git/msg374197.html
            unset GIT_DIR

            ARGS=(hook-impl project3/ --skip=project2/ --hook-type=pre-commit --script-version=3)

            HERE="$(cd "$(dirname "$0")" && pwd)"
            ARGS+=(--hook-dir "$HERE" -- "$@")
            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" "${ARGS[@]}"
            "#);
        }
    );

    // 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 => filters.clone() },
        {
            assert_snapshot!(context.read(".git/hooks/pre-commit"), @r#"
            #!/usr/bin/env bash
            # File generated by prek: https://github.com/j178/prek
            # ID: 182c10f181da4464a3eec51b83331688

            # Unset GIT_DIR to avoid git taking current directory as the git dir.
            # See:
            # https://github.com/pre-commit/pre-commit/issues/2295
            # https://www.spinics.net/lists/git/msg374197.html
            unset GIT_DIR

            ARGS=(hook-impl project3/ --hook-type=pre-commit --script-version=3)

            HERE="$(cd "$(dirname "$0")" && pwd)"
            ARGS+=(--hook-dir "$HERE" -- "$@")
            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" "${ARGS[@]}"
            "#);
        }
    );

    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.install_hooks().arg("project3").arg("--skip").arg("project3/project5/"), @r"
    success: true
    exit_code: 0
    ----- stdout -----

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

    // Install all hooks
    cmd_snapshot!(context.filters(), context.install_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(CONFIG_FILE)
        .write_str(root_config)?;
    context.work_dir().child("project2").create_dir_all()?;
    context
        .work_dir()
        .child("project2")
        .child(CONFIG_FILE)
        .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#"
            #!/usr/bin/env bash
            # File generated by prek: https://github.com/j178/prek
            # ID: 182c10f181da4464a3eec51b83331688

            # Unset GIT_DIR to avoid git taking current directory as the git dir.
            # See:
            # https://github.com/pre-commit/pre-commit/issues/2295
            # https://www.spinics.net/lists/git/msg374197.html
            unset GIT_DIR

            ARGS=(hook-impl --hook-type=pre-commit --skip-on-missing-config --script-version=3)

            HERE="$(cd "$(dirname "$0")" && pwd)"
            ARGS+=(--hook-dir "$HERE" -- "$@")
            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" "${ARGS[@]}"
            "#);
        }
    );

    Ok(())
}
