use insta_cmd::assert_cmd_snapshot;

use crate::CliTest;

#[test]
fn exclude_scripts_only_applies_to_implicitly_discovered_files() -> anyhow::Result<()> {
    let case = CliTest::with_files([
        ("main.py", "value: int = 'project'"),
        (
            "script.py",
            r#"
            # /// script
            # dependencies = []
            # ///
            value: int = "script"
            "#,
        ),
        (
            "nested/script.py",
            r#"
            # /// script
            # dependencies = []
            # ///
            value: int = "nested-script"
            "#,
        ),
    ])?;

    assert_cmd_snapshot!(case.command().env("TY_OUTPUT_FORMAT", "concise"), @r#"
    success: false
    exit_code: 1
    ----- stdout -----
    main.py:1:14: error[invalid-assignment] Object of type `Literal["project"]` is not assignable to `int`
    nested/script.py:5:14: error[invalid-assignment] Object of type `Literal["nested-script"]` is not assignable to `int`
    script.py:5:14: error[invalid-assignment] Object of type `Literal["script"]` is not assignable to `int`
    Found 3 diagnostics

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

    assert_cmd_snapshot!(case.command().env("TY_OUTPUT_FORMAT", "concise").arg("--exclude-scripts"), @r#"
    success: false
    exit_code: 1
    ----- stdout -----
    main.py:1:14: error[invalid-assignment] Object of type `Literal["project"]` is not assignable to `int`
    Found 1 diagnostic

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

    assert_cmd_snapshot!(case.command().env("TY_OUTPUT_FORMAT", "concise").arg("--exclude-scripts").arg("script.py"), @r#"
    success: false
    exit_code: 1
    ----- stdout -----
    script.py:5:14: error[invalid-assignment] Object of type `Literal["script"]` is not assignable to `int`
    Found 1 diagnostic

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

    case.write_file(
        "ty.toml",
        r#"
        [src]
        exclude-scripts = true
        "#,
    )?;

    assert_cmd_snapshot!(case.command().env("TY_OUTPUT_FORMAT", "concise"), @r#"
    success: false
    exit_code: 1
    ----- stdout -----
    main.py:1:14: error[invalid-assignment] Object of type `Literal["project"]` is not assignable to `int`
    Found 1 diagnostic

    ----- stderr -----
    "#);
    assert_cmd_snapshot!(case.command().env("TY_OUTPUT_FORMAT", "concise").arg("--include-scripts"), @r#"
    success: false
    exit_code: 1
    ----- stdout -----
    main.py:1:14: error[invalid-assignment] Object of type `Literal["project"]` is not assignable to `int`
    nested/script.py:5:14: error[invalid-assignment] Object of type `Literal["nested-script"]` is not assignable to `int`
    script.py:5:14: error[invalid-assignment] Object of type `Literal["script"]` is not assignable to `int`
    Found 3 diagnostics

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

    Ok(())
}

/// Test exclude CLI argument functionality
#[test]
fn exclude_argument() -> anyhow::Result<()> {
    let case = CliTest::with_files([
        (
            "src/main.py",
            r#"
            print(undefined_var)  # error: unresolved-reference
            "#,
        ),
        (
            "tests/test_main.py",
            r#"
            print(another_undefined_var)  # error: unresolved-reference
            "#,
        ),
        (
            "temp_file.py",
            r#"
            print(temp_undefined_var)  # error: unresolved-reference
            "#,
        ),
    ])?;

    // Test that exclude argument is recognized and works
    assert_cmd_snapshot!(case.command().arg("--exclude").arg("tests/"), @"
    success: false
    exit_code: 1
    ----- stdout -----
    error[unresolved-reference]: Name `undefined_var` used when not defined
     --> src/main.py:2:7
      |
    2 | print(undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^

    error[unresolved-reference]: Name `temp_undefined_var` used when not defined
     --> temp_file.py:2:7
      |
    2 | print(temp_undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^^^^^^

    Found 2 diagnostics

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

    // Test multiple exclude patterns
    assert_cmd_snapshot!(case.command().arg("--exclude").arg("tests/").arg("--exclude").arg("temp_*.py"), @"
    success: false
    exit_code: 1
    ----- stdout -----
    error[unresolved-reference]: Name `undefined_var` used when not defined
     --> src/main.py:2:7
      |
    2 | print(undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^

    Found 1 diagnostic

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

    Ok(())
}

/// Test configuration file include functionality
#[test]
fn configuration_include() -> anyhow::Result<()> {
    let case = CliTest::with_files([
        (
            "src/main.py",
            r#"
            print(undefined_var)  # error: unresolved-reference
            "#,
        ),
        (
            "tests/test_main.py",
            r#"
            print(another_undefined_var)  # error: unresolved-reference
            "#,
        ),
        (
            "other.py",
            r#"
            print(other_undefined_var)  # error: unresolved-reference
            "#,
        ),
    ])?;

    // Test include via configuration - should only check included files
    case.write_file(
        "ty.toml",
        r#"
        [src]
        include = ["src"]
        "#,
    )?;

    assert_cmd_snapshot!(case.command(), @"
    success: false
    exit_code: 1
    ----- stdout -----
    error[unresolved-reference]: Name `undefined_var` used when not defined
     --> src/main.py:2:7
      |
    2 | print(undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^

    Found 1 diagnostic

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

    // Test multiple include patterns via configuration
    case.write_file(
        "ty.toml",
        r#"
        [src]
        include = ["src", "other.py"]
        "#,
    )?;

    assert_cmd_snapshot!(case.command(), @"
    success: false
    exit_code: 1
    ----- stdout -----
    error[unresolved-reference]: Name `other_undefined_var` used when not defined
     --> other.py:2:7
      |
    2 | print(other_undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^^^^^^^

    error[unresolved-reference]: Name `undefined_var` used when not defined
     --> src/main.py:2:7
      |
    2 | print(undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^

    Found 2 diagnostics

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

    Ok(())
}

/// Files without extensions can be included by adding a literal glob to `include` that matches
/// the path exactly. A literal glob is a glob without any meta characters.
#[test]
fn configuration_include_no_extension() -> anyhow::Result<()> {
    let case = CliTest::with_files([(
        "src/main",
        r#"
            print(undefined_var)  # error: unresolved-reference
            "#,
    )])?;

    // By default, `src/main` is excluded because the file has no supported extension.
    case.write_file(
        "ty.toml",
        r#"
        [src]
        include = ["src"]
        "#,
    )?;

    assert_cmd_snapshot!(case.command(), @"
    success: true
    exit_code: 0
    ----- stdout -----
    All checks passed!

    ----- stderr -----
    WARN No python files found under the given path(s)
    ");

    // The file can be included by adding an exactly matching pattern
    case.write_file(
        "ty.toml",
        r#"
        [src]
        include = ["src", "src/main"]
        "#,
    )?;

    assert_cmd_snapshot!(case.command(), @"
    success: false
    exit_code: 1
    ----- stdout -----
    error[unresolved-reference]: Name `undefined_var` used when not defined
     --> src/main:2:7
      |
    2 | print(undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^

    Found 1 diagnostic

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

    Ok(())
}

/// Test configuration file exclude functionality
#[test]
fn configuration_exclude() -> anyhow::Result<()> {
    let case = CliTest::with_files([
        (
            "src/main.py",
            r#"
            print(undefined_var)  # error: unresolved-reference
            "#,
        ),
        (
            "tests/test_main.py",
            r#"
            print(another_undefined_var)  # error: unresolved-reference
            "#,
        ),
        (
            "temp_file.py",
            r#"
            print(temp_undefined_var)  # error: unresolved-reference
            "#,
        ),
    ])?;

    // Test exclude via configuration
    case.write_file(
        "ty.toml",
        r#"
        [src]
        exclude = ["tests/"]
        "#,
    )?;

    assert_cmd_snapshot!(case.command(), @"
    success: false
    exit_code: 1
    ----- stdout -----
    error[unresolved-reference]: Name `undefined_var` used when not defined
     --> src/main.py:2:7
      |
    2 | print(undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^

    error[unresolved-reference]: Name `temp_undefined_var` used when not defined
     --> temp_file.py:2:7
      |
    2 | print(temp_undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^^^^^^

    Found 2 diagnostics

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

    // Test multiple exclude patterns via configuration
    case.write_file(
        "ty.toml",
        r#"
        [src]
        exclude = ["tests/", "temp_*.py"]
        "#,
    )?;

    assert_cmd_snapshot!(case.command(), @"
    success: false
    exit_code: 1
    ----- stdout -----
    error[unresolved-reference]: Name `undefined_var` used when not defined
     --> src/main.py:2:7
      |
    2 | print(undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^

    Found 1 diagnostic

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

    Ok(())
}

/// Test that exclude takes precedence over include in configuration
#[test]
fn exclude_precedence_over_include() -> anyhow::Result<()> {
    let case = CliTest::with_files([
        (
            "src/main.py",
            r#"
            print(undefined_var)  # error: unresolved-reference
            "#,
        ),
        (
            "src/test_helper.py",
            r#"
            print(helper_undefined_var)  # error: unresolved-reference
            "#,
        ),
        (
            "other.py",
            r#"
            print(other_undefined_var)  # error: unresolved-reference
            "#,
        ),
    ])?;

    // Include all src files but exclude test files - exclude should win
    case.write_file(
        "ty.toml",
        r#"
        [src]
        include = ["src"]
        exclude = ["**/test_*.py"]
        "#,
    )?;

    assert_cmd_snapshot!(case.command(), @"
    success: false
    exit_code: 1
    ----- stdout -----
    error[unresolved-reference]: Name `undefined_var` used when not defined
     --> src/main.py:2:7
      |
    2 | print(undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^

    Found 1 diagnostic

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

    Ok(())
}

/// Test that CLI exclude overrides configuration include
#[test]
fn exclude_argument_precedence_include_argument() -> anyhow::Result<()> {
    let case = CliTest::with_files([
        (
            "src/main.py",
            r#"
            print(undefined_var)  # error: unresolved-reference
            "#,
        ),
        (
            "tests/test_main.py",
            r#"
            print(another_undefined_var)  # error: unresolved-reference
            "#,
        ),
        (
            "other.py",
            r#"
            print(other_undefined_var)  # error: unresolved-reference
            "#,
        ),
    ])?;

    // Configuration includes all files, but CLI excludes tests
    case.write_file(
        "ty.toml",
        r#"
        [src]
        include = ["src/", "tests/"]
        "#,
    )?;

    assert_cmd_snapshot!(case.command().arg("--exclude").arg("tests/"), @"
    success: false
    exit_code: 1
    ----- stdout -----
    error[unresolved-reference]: Name `undefined_var` used when not defined
     --> src/main.py:2:7
      |
    2 | print(undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^

    Found 1 diagnostic

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

    Ok(())
}

/// Test that default excludes can be removed using negated patterns
#[test]
fn remove_default_exclude() -> anyhow::Result<()> {
    let case = CliTest::with_files([
        (
            "src/main.py",
            r#"
            print(undefined_var)  # error: unresolved-reference
            "#,
        ),
        (
            "dist/generated.py",
            r#"
            print(another_undefined_var)  # error: unresolved-reference
            "#,
        ),
    ])?;

    // By default, 'dist' directory should be excluded (see default excludes)
    assert_cmd_snapshot!(case.command(), @"
    success: false
    exit_code: 1
    ----- stdout -----
    error[unresolved-reference]: Name `undefined_var` used when not defined
     --> src/main.py:2:7
      |
    2 | print(undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^

    Found 1 diagnostic

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

    // Now override the default exclude by using a negated pattern to re-include 'dist'
    case.write_file(
        "ty.toml",
        r#"
        [src]
        exclude = ["!**/dist/"]
        "#,
    )?;

    assert_cmd_snapshot!(case.command(), @"
    success: false
    exit_code: 1
    ----- stdout -----
    error[unresolved-reference]: Name `another_undefined_var` used when not defined
     --> dist/generated.py:2:7
      |
    2 | print(another_undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^^^^^^^^^

    error[unresolved-reference]: Name `undefined_var` used when not defined
     --> src/main.py:2:7
      |
    2 | print(undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^

    Found 2 diagnostics

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

    Ok(())
}

/// Test that configuration excludes can be removed via CLI negation
#[test]
fn cli_removes_config_exclude() -> anyhow::Result<()> {
    let case = CliTest::with_files([
        (
            "src/main.py",
            r#"
            print(undefined_var)  # error: unresolved-reference
            "#,
        ),
        (
            "build/output.py",
            r#"
            print(build_undefined_var)  # error: unresolved-reference
            "#,
        ),
    ])?;

    // Configuration excludes the build directory
    case.write_file(
        "ty.toml",
        r#"
        [src]
        exclude = ["build/"]
        "#,
    )?;

    // Verify that build/ is excluded by configuration
    assert_cmd_snapshot!(case.command(), @"
    success: false
    exit_code: 1
    ----- stdout -----
    error[unresolved-reference]: Name `undefined_var` used when not defined
     --> src/main.py:2:7
      |
    2 | print(undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^

    Found 1 diagnostic

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

    // Now remove the configuration exclude via CLI negation
    assert_cmd_snapshot!(case.command().arg("--exclude").arg("!build/"), @"
    success: false
    exit_code: 1
    ----- stdout -----
    error[unresolved-reference]: Name `build_undefined_var` used when not defined
     --> build/output.py:2:7
      |
    2 | print(build_undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^^^^^^^

    error[unresolved-reference]: Name `undefined_var` used when not defined
     --> src/main.py:2:7
      |
    2 | print(undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^

    Found 2 diagnostics

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

    Ok(())
}

/// Test behavior when explicitly checking a path that matches an exclude pattern
#[test]
fn explicit_path_overrides_exclude() -> anyhow::Result<()> {
    let case = CliTest::with_files([
        (
            "src/main.py",
            r#"
            print(undefined_var)  # error: unresolved-reference
            "#,
        ),
        (
            "tests/generated.py",
            r#"
            print(dist_undefined_var)  # error: unresolved-reference
            "#,
        ),
        (
            "dist/other.py",
            r#"
            print(other_undefined_var)  # error: unresolved-reference
            "#,
        ),
        (
            "ty.toml",
            r#"
            [src]
            exclude = ["tests/generated.py"]
            "#,
        ),
    ])?;

    // dist is excluded by default and `tests/generated` is excluded in the project, so only src/main.py should be checked
    assert_cmd_snapshot!(case.command(), @"
    success: false
    exit_code: 1
    ----- stdout -----
    error[unresolved-reference]: Name `undefined_var` used when not defined
     --> src/main.py:2:7
      |
    2 | print(undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^

    Found 1 diagnostic

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

    // Explicitly checking a file in an excluded directory should still check that file
    assert_cmd_snapshot!(case.command().arg("tests/generated.py"), @"
    success: false
    exit_code: 1
    ----- stdout -----
    error[unresolved-reference]: Name `dist_undefined_var` used when not defined
     --> tests/generated.py:2:7
      |
    2 | print(dist_undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^^^^^^

    Found 1 diagnostic

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

    // Explicitly checking the entire excluded directory should check all files in it
    assert_cmd_snapshot!(case.command().arg("dist/"), @"
    success: false
    exit_code: 1
    ----- stdout -----
    error[unresolved-reference]: Name `other_undefined_var` used when not defined
     --> dist/other.py:2:7
      |
    2 | print(other_undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^^^^^^^

    Found 1 diagnostic

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

    Ok(())
}

/// Test behavior when explicitly checking a path that matches an exclude pattern and `--force-exclude` is provided
#[test]
fn explicit_path_overrides_exclude_force_exclude() -> anyhow::Result<()> {
    let case = CliTest::with_files([
        (
            "src/main.py",
            r#"
            print(undefined_var)  # error: unresolved-reference
            "#,
        ),
        (
            "tests/generated.py",
            r#"
            print(dist_undefined_var)  # error: unresolved-reference
            "#,
        ),
        (
            "dist/other.py",
            r#"
            print(other_undefined_var)  # error: unresolved-reference
            "#,
        ),
        (
            "ty.toml",
            r#"
            [src]
            exclude = ["tests/generated.py"]
            "#,
        ),
    ])?;

    // Explicitly checking a file in an excluded directory should still check that file
    assert_cmd_snapshot!(case.command().arg("tests/generated.py").arg("src/main.py"), @"
    success: false
    exit_code: 1
    ----- stdout -----
    error[unresolved-reference]: Name `undefined_var` used when not defined
     --> src/main.py:2:7
      |
    2 | print(undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^

    error[unresolved-reference]: Name `dist_undefined_var` used when not defined
     --> tests/generated.py:2:7
      |
    2 | print(dist_undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^^^^^^

    Found 2 diagnostics

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

    // Except when `--force-exclude` is set.
    assert_cmd_snapshot!(case.command().arg("tests/generated.py").arg("src/main.py").arg("--force-exclude"), @"
    success: false
    exit_code: 1
    ----- stdout -----
    error[unresolved-reference]: Name `undefined_var` used when not defined
     --> src/main.py:2:7
      |
    2 | print(undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^

    Found 1 diagnostic

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

    // Explicitly checking the entire excluded directory should check all files in it
    assert_cmd_snapshot!(case.command().arg("dist/").arg("src/main.py"), @"
    success: false
    exit_code: 1
    ----- stdout -----
    error[unresolved-reference]: Name `other_undefined_var` used when not defined
     --> dist/other.py:2:7
      |
    2 | print(other_undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^^^^^^^

    error[unresolved-reference]: Name `undefined_var` used when not defined
     --> src/main.py:2:7
      |
    2 | print(undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^

    Found 2 diagnostics

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

    // Except when using `--force-exclude`
    assert_cmd_snapshot!(case.command().arg("dist/").arg("src/main.py").arg("--force-exclude"), @"
    success: false
    exit_code: 1
    ----- stdout -----
    error[unresolved-reference]: Name `undefined_var` used when not defined
     --> src/main.py:2:7
      |
    2 | print(undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^

    Found 1 diagnostic

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

    Ok(())
}

/// Test that `--force-exclude` respects exclude patterns even for explicitly passed files.
#[test]
fn force_exclude_directory_exclusion() -> anyhow::Result<()> {
    let case = CliTest::with_files([
        (
            "src/main.py",
            r#"
            print(undefined_var)  # error: unresolved-reference
            "#,
        ),
        (
            "out/amd64/install/_setup_util.py",
            r#"
            base_path: str = "/path"
            if base_path not in CMAKE_PREFIX_PATH:
                CMAKE_PREFIX_PATH.insert(0, base_path)
            "#,
        ),
        (
            "ty.toml",
            r#"
            [src]
            exclude = ["out"]
            "#,
        ),
    ])?;

    // Without --force-exclude, explicitly passed file overrides exclude.
    assert_cmd_snapshot!(case.command().arg("out/amd64/install/_setup_util.py"), @"
    success: false
    exit_code: 1
    ----- stdout -----
    error[unresolved-reference]: Name `CMAKE_PREFIX_PATH` used when not defined
     --> out/amd64/install/_setup_util.py:3:21
      |
    3 | if base_path not in CMAKE_PREFIX_PATH:
      |                     ^^^^^^^^^^^^^^^^^

    error[unresolved-reference]: Name `CMAKE_PREFIX_PATH` used when not defined
     --> out/amd64/install/_setup_util.py:4:5
      |
    4 |     CMAKE_PREFIX_PATH.insert(0, base_path)
      |     ^^^^^^^^^^^^^^^^^

    Found 2 diagnostics

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

    // With --force-exclude, the exclude pattern is enforced even for explicit paths.
    assert_cmd_snapshot!(case.command().arg("--force-exclude").arg("out/amd64/install/_setup_util.py"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    All checks passed!

    ----- stderr -----
    WARN No python files found under the given path(s)
    ");

    Ok(())
}

#[test]
fn cli_and_configuration_exclude() -> anyhow::Result<()> {
    let case = CliTest::with_files([
        (
            "src/main.py",
            r#"
            print(undefined_var)  # error: unresolved-reference
            "#,
        ),
        (
            "tests/generated.py",
            r#"
            print(dist_undefined_var)  # error: unresolved-reference
            "#,
        ),
        (
            "my_dist/other.py",
            r#"
            print(other_undefined_var)  # error: unresolved-reference
            "#,
        ),
        (
            "ty.toml",
            r#"
            [src]
            exclude = ["tests/"]
            "#,
        ),
    ])?;

    assert_cmd_snapshot!(case.command(), @"
    success: false
    exit_code: 1
    ----- stdout -----
    error[unresolved-reference]: Name `other_undefined_var` used when not defined
     --> my_dist/other.py:2:7
      |
    2 | print(other_undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^^^^^^^

    error[unresolved-reference]: Name `undefined_var` used when not defined
     --> src/main.py:2:7
      |
    2 | print(undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^

    Found 2 diagnostics

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

    assert_cmd_snapshot!(case.command().arg("--exclude").arg("my_dist/"), @"
    success: false
    exit_code: 1
    ----- stdout -----
    error[unresolved-reference]: Name `undefined_var` used when not defined
     --> src/main.py:2:7
      |
    2 | print(undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^

    Found 1 diagnostic

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

    Ok(())
}

#[test]
fn invalid_include_pattern() -> anyhow::Result<()> {
    let case = CliTest::with_files([
        (
            "src/main.py",
            r#"
            print(undefined_var)  # error: unresolved-reference
            "#,
        ),
        (
            "ty.toml",
            r#"
            [src]
            include = [
                "src/**test/"
            ]
            "#,
        ),
    ])?;

    // By default, dist/ is excluded, so only src/main.py should be checked
    assert_cmd_snapshot!(case.command(), @r#"
    success: false
    exit_code: 2
    ----- stdout -----

    ----- stderr -----
    ty failed
      Cause: error[invalid-glob]: Invalid pattern
     --> ty.toml:4:5
      |
    2 | [src]
    3 | include = [
    4 |     "src/**test/"
      |     ^^^^^^^^^^^^^ Too many stars at position 5
    5 | ]
      |
    "#);

    Ok(())
}

#[test]
fn invalid_include_pattern_concise_output() -> anyhow::Result<()> {
    let case = CliTest::with_files([
        (
            "src/main.py",
            r#"
            print(undefined_var)  # error: unresolved-reference
            "#,
        ),
        (
            "ty.toml",
            r#"
            [src]
            include = [
                "src/**test/"
            ]
            "#,
        ),
    ])?;

    // By default, dist/ is excluded, so only src/main.py should be checked
    assert_cmd_snapshot!(case.command().arg("--output-format").arg("concise"), @"
    success: false
    exit_code: 2
    ----- stdout -----

    ----- stderr -----
    ty failed
      Cause: ty.toml:4:5: error[invalid-glob] Invalid include pattern `src/**test/`: Too many stars at position 5
    ");

    Ok(())
}

#[test]
fn invalid_exclude_pattern() -> anyhow::Result<()> {
    let case = CliTest::with_files([
        (
            "src/main.py",
            r#"
            print(undefined_var)  # error: unresolved-reference
            "#,
        ),
        (
            "ty.toml",
            r#"
            [src]
            exclude = [
                "../src"
            ]
            "#,
        ),
    ])?;

    // By default, dist/ is excluded, so only src/main.py should be checked
    assert_cmd_snapshot!(case.command(), @r#"
    success: false
    exit_code: 2
    ----- stdout -----

    ----- stderr -----
    ty failed
      Cause: error[invalid-glob]: Invalid pattern
     --> ty.toml:4:5
      |
    2 | [src]
    3 | exclude = [
    4 |     "../src"
      |     ^^^^^^^^ The parent directory operator (`..`) at position 1 is not allowed
    5 | ]
      |
    "#);

    Ok(())
}

/// Test that ty works correctly with Bazel's symlinked file structure
#[test]
#[cfg(unix)]
fn bazel_symlinked_files() -> anyhow::Result<()> {
    let case = CliTest::with_files([
        // Original source files in the project
        (
            "main.py",
            r#"
import library

result = library.process_data()
print(undefined_var)  # error: unresolved-reference
            "#,
        ),
        (
            "library.py",
            r#"
def process_data():
    return missing_value  # error: unresolved-reference
            "#,
        ),
        // Another source file that won't be symlinked
        (
            "other.py",
            r#"
print(other_undefined)  # error: unresolved-reference
            "#,
        ),
    ])?;

    // Create Bazel-style symlinks pointing to the actual source files
    // Bazel typically creates symlinks in bazel-out/k8-fastbuild/bin/ that point to actual sources
    std::fs::create_dir_all(case.project_dir.join("bazel-out/k8-fastbuild/bin"))?;

    // Use absolute paths to ensure the symlinks work correctly
    case.write_symlink(
        case.project_dir.join("main.py"),
        "bazel-out/k8-fastbuild/bin/main.py",
    )?;
    case.write_symlink(
        case.project_dir.join("library.py"),
        "bazel-out/k8-fastbuild/bin/library.py",
    )?;

    // Change to the bazel-out directory and run ty from there
    // The symlinks should be followed and errors should be found
    assert_cmd_snapshot!(case.command().current_dir(case.project_dir.join("bazel-out/k8-fastbuild/bin")), @"
    success: false
    exit_code: 1
    ----- stdout -----
    error[unresolved-reference]: Name `missing_value` used when not defined
     --> library.py:3:12
      |
    3 |     return missing_value  # error: unresolved-reference
      |            ^^^^^^^^^^^^^

    error[unresolved-reference]: Name `undefined_var` used when not defined
     --> main.py:5:7
      |
    5 | print(undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^

    Found 2 diagnostics

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

    // Test that when checking a specific symlinked file from the bazel-out directory, it works correctly
    assert_cmd_snapshot!(case.command().current_dir(case.project_dir.join("bazel-out/k8-fastbuild/bin")).arg("main.py"), @"
    success: false
    exit_code: 1
    ----- stdout -----
    error[unresolved-reference]: Name `undefined_var` used when not defined
     --> main.py:5:7
      |
    5 | print(undefined_var)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^

    Found 1 diagnostic

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

    Ok(())
}

/// Test that exclude patterns match on symlink source names, not target names
#[test]
#[cfg(unix)]
fn exclude_symlink_source_not_target() -> anyhow::Result<()> {
    let case = CliTest::with_files([
        // Target files with generic names
        (
            "src/module.py",
            r#"
def process():
    return undefined_var  # error: unresolved-reference
            "#,
        ),
        (
            "src/utils.py",
            r#"
def helper():
    return missing_value  # error: unresolved-reference
            "#,
        ),
        (
            "regular.py",
            r#"
print(regular_undefined)  # error: unresolved-reference
            "#,
        ),
    ])?;

    // Create symlinks with names that differ from their targets
    // This simulates build systems that rename files during symlinking
    case.write_symlink("src/module.py", "generated_module.py")?;
    case.write_symlink("src/utils.py", "generated_utils.py")?;

    // Exclude pattern should match on the symlink name (generated_*), not the target name
    assert_cmd_snapshot!(case.command().arg("--exclude").arg("generated_*.py"), @"
    success: false
    exit_code: 1
    ----- stdout -----
    error[unresolved-reference]: Name `regular_undefined` used when not defined
     --> regular.py:2:7
      |
    2 | print(regular_undefined)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^^^^^

    error[unresolved-reference]: Name `undefined_var` used when not defined
     --> src/module.py:3:12
      |
    3 |     return undefined_var  # error: unresolved-reference
      |            ^^^^^^^^^^^^^

    error[unresolved-reference]: Name `missing_value` used when not defined
     --> src/utils.py:3:12
      |
    3 |     return missing_value  # error: unresolved-reference
      |            ^^^^^^^^^^^^^

    Found 3 diagnostics

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

    // Exclude pattern on target path should not affect symlinks with different names
    assert_cmd_snapshot!(case.command().arg("--exclude").arg("src/*.py"), @"
    success: false
    exit_code: 1
    ----- stdout -----
    error[unresolved-reference]: Name `undefined_var` used when not defined
     --> generated_module.py:3:12
      |
    3 |     return undefined_var  # error: unresolved-reference
      |            ^^^^^^^^^^^^^

    error[unresolved-reference]: Name `missing_value` used when not defined
     --> generated_utils.py:3:12
      |
    3 |     return missing_value  # error: unresolved-reference
      |            ^^^^^^^^^^^^^

    error[unresolved-reference]: Name `regular_undefined` used when not defined
     --> regular.py:2:7
      |
    2 | print(regular_undefined)  # error: unresolved-reference
      |       ^^^^^^^^^^^^^^^^^

    Found 3 diagnostics

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

    // Test that explicitly passing a symlink always checks it, even if excluded
    assert_cmd_snapshot!(case.command().arg("--exclude").arg("generated_*.py").arg("generated_module.py"), @"
    success: false
    exit_code: 1
    ----- stdout -----
    error[unresolved-reference]: Name `undefined_var` used when not defined
     --> generated_module.py:3:12
      |
    3 |     return undefined_var  # error: unresolved-reference
      |            ^^^^^^^^^^^^^

    Found 1 diagnostic

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

    Ok(())
}
