//! Configuration discovery and functionality tests.

use crate::common::{OutputMode, input_under_test, zizmor};

/// Ensures we correctly discover a configuration file at the root
/// of a given input directory, i.e. `config-in-root/zizmor.yml` in
/// this case.
#[test]
fn test_discovers_config_in_root() -> anyhow::Result<()> {
    insta::assert_snapshot!(
        zizmor()
            .input(input_under_test("config-scenarios/config-in-root"))
            .setenv("RUST_LOG", "zizmor::config=debug")
            .output(OutputMode::Both)
            .run()?,
        @"
    DEBUG zizmor::config: discovering config for local input `@@INPUT@@`
    DEBUG zizmor::config: attempting config discovery in `@@INPUT@@`
    DEBUG zizmor::config: found config candidate at `@@INPUT@@/zizmor.yml`
    No findings to report. Good job! (1 ignored, 1 suppressed)
    "
    );

    Ok(())
}

/// Ensures we correctly discover a configuration file in the root
/// directory from an input filename, i.e. going from
/// `config-in-root/.github/workflows/hackme.yml`
/// to `config-in-root/zizmor.yml` in this case.
#[test]
fn test_discovers_config_in_root_from_file_input() -> anyhow::Result<()> {
    insta::assert_snapshot!(
        zizmor()
            .input(input_under_test(
                "config-scenarios/config-in-root/.github/workflows/hackme.yml"
            ))
            .setenv("RUST_LOG", "zizmor::config=debug")
            .output(OutputMode::Both)
            .run()?,
        @"
    DEBUG zizmor::config: discovering config for local input `@@INPUT@@`
    DEBUG zizmor::config: attempting config discovery in `@@TEST_PREFIX@@/config-scenarios/config-in-root/.github/workflows`
    DEBUG zizmor::config: found config candidate at `@@TEST_PREFIX@@/config-scenarios/config-in-root/zizmor.yml`
    No findings to report. Good job! (1 ignored, 1 suppressed)
    "
    );

    Ok(())
}

/// Ensures we correctly discover a configuration file in the root
/// directory from a child input directory, i.e. going from
/// `config-in-root/.github/workflows/` to `config-in-root/zizmor.yml`
/// in this case.
#[test]
fn test_discovers_config_in_root_from_child_dir() -> anyhow::Result<()> {
    insta::assert_snapshot!(
        zizmor()
            .input(input_under_test(
                "config-scenarios/config-in-root/.github/workflows"
            ))
            .setenv("RUST_LOG", "zizmor::config=debug")
            .output(OutputMode::Both)
            .run()?,
        @"
    DEBUG zizmor::config: discovering config for local input `@@INPUT@@`
    DEBUG zizmor::config: attempting config discovery in `@@INPUT@@`
    DEBUG zizmor::config: found config candidate at `@@TEST_PREFIX@@/config-scenarios/config-in-root/zizmor.yml`
    No findings to report. Good job! (1 ignored, 1 suppressed)
    "
    );

    Ok(())
}

/// Ensures we ignore a configuration file in the root of a given
/// input directory when `--no-config` is specified.
#[test]
fn test_ignores_config_in_root() -> anyhow::Result<()> {
    insta::assert_snapshot!(
        zizmor()
            .no_config(true)
            .input(input_under_test("config-scenarios/config-in-root"))
            .setenv("RUST_LOG", "zizmor::config=debug")
            .output(OutputMode::Stderr)
            .run()?,
        @"DEBUG zizmor::config: skipping config discovery: explicitly disabled"
    );

    Ok(())
}

/// Ensures we ignore a configuration file in the root directory
/// from an input filename when `--no-config` is specified.
#[test]
fn test_ignores_config_in_root_from_file_input() -> anyhow::Result<()> {
    insta::assert_snapshot!(
        zizmor()
            .no_config(true)
            .input(input_under_test(
                "config-scenarios/config-in-root/.github/workflows/hackme.yml"
            ))
            .setenv("RUST_LOG", "zizmor::config=debug")
            .output(OutputMode::Stderr)
            .run()?,
        @"DEBUG zizmor::config: skipping config discovery: explicitly disabled"
    );

    Ok(())
}

/// Ensures we ignore a configuration file in the root directory
/// from a child input directory when `--no-config` is specified.
#[test]
fn test_ignores_config_in_root_from_child_dir() -> anyhow::Result<()> {
    insta::assert_snapshot!(
        zizmor()
            .no_config(true)
            .input(input_under_test(
                "config-scenarios/config-in-root/.github/workflows"
            ))
            .setenv("RUST_LOG", "zizmor::config=debug")
            .output(OutputMode::Stderr)
            .run()?,
        @"DEBUG zizmor::config: skipping config discovery: explicitly disabled"
    );

    Ok(())
}

/// Ensures we correctly discover a configuration file in a `.github`
/// subdirectory of a given input directory, i.e.
/// `config-in-dotgithub/.github/zizmor.yml` in this case.
#[test]
fn test_discovers_config_in_dotgithub() -> anyhow::Result<()> {
    insta::assert_snapshot!(
        zizmor()
            .input(input_under_test("config-scenarios/config-in-dotgithub"))
            .setenv("RUST_LOG", "zizmor::config=debug")
            .output(OutputMode::Both)
            .run()?,
        @"
    DEBUG zizmor::config: discovering config for local input `@@INPUT@@`
    DEBUG zizmor::config: attempting config discovery in `@@INPUT@@`
    DEBUG zizmor::config: found config candidate at `@@INPUT@@/.github/zizmor.yml`
    No findings to report. Good job! (1 ignored, 1 suppressed)
    ",
    );

    Ok(())
}

/// Ensures we correctly discover a `zizmor.yaml` configuration file in a `.github`
/// subdirectory of a given input directory, i.e.
/// `config-in-dotgithub/.github/zizmor.yaml` in this case.
///
/// This tests that both `.yml` and `.yaml` extensions are supported.
#[test]
fn test_discovers_dotyaml_config_in_dotgithub() -> anyhow::Result<()> {
    insta::assert_snapshot!(
        zizmor()
            .input(input_under_test("config-scenarios/dotyaml-config-in-dotgithub"))
            .setenv("RUST_LOG", "zizmor::config=debug")
            .output(OutputMode::Both)
            .run()?,
        @"
    DEBUG zizmor::config: discovering config for local input `@@INPUT@@`
    DEBUG zizmor::config: attempting config discovery in `@@INPUT@@`
    DEBUG zizmor::config: found config candidate at `@@INPUT@@/.github/zizmor.yaml`
    No findings to report. Good job! (1 ignored, 1 suppressed)
    ",
    );

    Ok(())
}

/// Ensures we correctly discover a configuration file in a `.github`
/// subdirectory from an input filename, i.e. going from
/// `config-in-dotgithub/.github/workflows/hackme.yml`
/// to `config-in-dotgithub/.github/zizmor.yml` in this case.
#[test]
fn test_discovers_config_in_dotgithub_from_file_input() -> anyhow::Result<()> {
    insta::assert_snapshot!(
        zizmor()
            .input(input_under_test(
                "config-scenarios/config-in-dotgithub/.github/workflows/hackme.yml"
            ))
            .setenv("RUST_LOG", "zizmor::config=debug")
            .output(OutputMode::Both)
            .run()?,
        @"
    DEBUG zizmor::config: discovering config for local input `@@INPUT@@`
    DEBUG zizmor::config: attempting config discovery in `@@TEST_PREFIX@@/config-scenarios/config-in-dotgithub/.github/workflows`
    DEBUG zizmor::config: found config candidate at `@@TEST_PREFIX@@/config-scenarios/config-in-dotgithub/.github/zizmor.yml`
    No findings to report. Good job! (1 ignored, 1 suppressed)
    "
    );

    Ok(())
}

/// Ensures we ignore a configuration file in a `.github` subdirectory
/// of a given input directory when `--no-config` is specified.
#[test]
fn test_ignores_config_in_dotgithub() -> anyhow::Result<()> {
    insta::assert_snapshot!(
        zizmor()
            .no_config(true)
            .input(input_under_test("config-scenarios/config-in-dotgithub"))
            .setenv("RUST_LOG", "zizmor::config=debug")
            .output(OutputMode::Stderr)
            .run()?,
        @"DEBUG zizmor::config: skipping config discovery: explicitly disabled"
    );

    Ok(())
}

/// Ensures we ignore a configuration file in a `.github` subdirectory
/// from an input filename when `--no-config` is specified.
#[test]
fn test_ignores_config_in_dotgithub_from_file_input() -> anyhow::Result<()> {
    insta::assert_snapshot!(
        zizmor()
            .no_config(true)
            .input(input_under_test(
                "config-scenarios/config-in-dotgithub/.github/workflows/hackme.yml"
            ))
            .setenv("RUST_LOG", "zizmor::config=debug")
            .output(OutputMode::Stderr)
            .run()?,
        @"DEBUG zizmor::config: skipping config discovery: explicitly disabled"
    );

    Ok(())
}

/// Ensures we respect the `disable: true` configuration directive.
#[test]
fn test_disablement() -> anyhow::Result<()> {
    insta::assert_snapshot!(
        zizmor()
            .input(input_under_test("config-scenarios/disablement"))
            .setenv("RUST_LOG", "zizmor::audit=debug")
            .output(OutputMode::Both)
            .run()?,
        @r#"
    DEBUG audit{input=Workflow(file://@@INPUT@@/.github/workflows/hackme.yml)}: zizmor::audit: skipping: template-injection is disabled in config for group Group("@@INPUT@@")
    No findings to report. Good job! (1 suppressed)
    "#
    );

    Ok(())
}

/// Various invalid config scenarios.
#[test]
fn test_invalid_configs() -> anyhow::Result<()> {
    // Top-level config schema is invalid.
    insta::assert_snapshot!(
        zizmor()
            .expects_failure(1)
            .input(input_under_test("neutral.yml"))
            .config(input_under_test(
                "config-scenarios/zizmor.invalid-schema-1.yml"
            ))
            .output(OutputMode::Stderr)
            .run()?,
        @"
     INFO zizmor: 🌈 zizmor v@@VERSION@@
    fatal: no audit was performed
    error: configuration error in @@CONFIG@@
      |
      = help: check your configuration file for syntax errors
      = help: see: https://docs.zizmor.sh/configuration/

    Caused by:
        0: configuration error in @@CONFIG@@
        1: invalid configuration syntax
        2: unknown field `rule`, expected `rules` at line 4 column 1
    "
    );

    Ok(())
}

/// Ensures that severity remapping changes the displayed severity of a finding.
/// artipacked normally produces Medium; remapped to High here.
#[test]
fn test_severity_remap() -> anyhow::Result<()> {
    insta::assert_snapshot!(
        zizmor()
            .input(input_under_test("config-scenarios/severity-remap"))
            .run()?,
        @"
    error[artipacked]: credential persistence through GitHub Actions artifacts
      --> @@INPUT@@/.github/workflows/hackme.yml:12:9
       |
    12 |       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag=v4.2.2
       |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ does not set persist-credentials: false
       |
       = note: audit confidence → Low
       = note: this finding has an auto-fix

    2 findings (1 suppressed, 1 unsafe fixes): 0 informational, 0 low, 0 medium, 1 high
    "
    );

    Ok(())
}

/// Ensures that remapped severity affects --min-severity filtering.
/// A Medium finding remapped to High must survive --min-severity=high.
#[test]
fn test_severity_remap_affects_min_severity() -> anyhow::Result<()> {
    insta::assert_snapshot!(
        zizmor()
            .input(input_under_test("config-scenarios/severity-remap"))
            .args(["--min-severity=high"])
            .run()?,
        @"
    error[artipacked]: credential persistence through GitHub Actions artifacts
      --> @@INPUT@@/.github/workflows/hackme.yml:12:9
       |
    12 |       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag=v4.2.2
       |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ does not set persist-credentials: false
       |
       = note: audit confidence → Low
       = note: this finding has an auto-fix

    2 findings (1 suppressed, 1 unsafe fixes): 0 informational, 0 low, 0 medium, 1 high
    "
    );

    Ok(())
}

/// Without remap config, an artipacked Medium finding is filtered out by --min-severity=high.
#[test]
fn test_no_remap_filtered_by_min_severity() -> anyhow::Result<()> {
    insta::assert_snapshot!(
        zizmor()
            .no_config(true)
            .input(input_under_test("config-scenarios/severity-remap"))
            .args(["--min-severity=high"])
            .run()?,
        @"No findings to report. Good job! (1 ignored, 1 suppressed)"
    );

    Ok(())
}

/// Ensures that even if we remap the severity of a finding, it does not affect the output when we
/// disable the configuration. This is the no-config counterpart to
/// [`test_severity_remap_affects_min_severity`].
#[test]
fn test_severity_remap_is_negated_by_no_config() -> anyhow::Result<()> {
    insta::assert_snapshot!(
        zizmor()
            .input(input_under_test("config-scenarios/severity-remap"))
            .args(["--min-severity=high", "--no-config"])
            .run()?,
        @"No findings to report. Good job! (1 ignored, 1 suppressed)"
    );

    Ok(())
}
