use anyhow::Result;
use assert_cmd::assert::OutputAssertExt;
use assert_fs::prelude::*;
use indoc::{formatdoc, indoc};
use insta::assert_snapshot;
use url::Url;

use uv_static::EnvVars;
use uv_test::uv_snapshot;

#[test]
fn nested_dependencies() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = [
            "scikit-learn==1.4.1.post1"
        ]
    "#,
    )?;

    uv_snapshot!(context.filters(), context.tree().arg("--universal"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    └── scikit-learn v1.4.1.post1
        ├── joblib v1.3.2
        ├── numpy v1.26.4
        ├── scipy v1.12.0
        │   └── numpy v1.26.4
        └── threadpoolctl v3.4.0

    ----- stderr -----
    Resolved 6 packages in [TIME]
    "
    );

    // `uv tree` should update the lockfile
    let lock = context.read("uv.lock");
    assert!(!lock.is_empty());

    Ok(())
}

#[test]
fn nested_platform_dependencies() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = [
            "jupyter-client"
        ]
    "#,
    )?;

    uv_snapshot!(context.filters(), context.tree().arg("--python-platform").arg("linux"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    └── jupyter-client v8.6.1
        ├── jupyter-core v5.7.2
        │   ├── platformdirs v4.2.0
        │   └── traitlets v5.14.2
        ├── python-dateutil v2.9.0.post0
        │   └── six v1.16.0
        ├── pyzmq v25.1.2
        ├── tornado v6.4
        └── traitlets v5.14.2

    ----- stderr -----
    Resolved 12 packages in [TIME]
    "
    );

    uv_snapshot!(context.filters(), context.tree().arg("--universal"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    └── jupyter-client v8.6.1
        ├── jupyter-core v5.7.2
        │   ├── platformdirs v4.2.0
        │   ├── pywin32 v306
        │   └── traitlets v5.14.2
        ├── python-dateutil v2.9.0.post0
        │   └── six v1.16.0
        ├── pyzmq v25.1.2
        │   └── cffi v1.16.0
        │       └── pycparser v2.21
        ├── tornado v6.4
        └── traitlets v5.14.2

    ----- stderr -----
    Resolved 12 packages in [TIME]
    "
    );

    // `uv tree` should update the lockfile
    let lock = context.read("uv.lock");
    assert!(!lock.is_empty());

    Ok(())
}

#[test]
fn invert() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = [
            "scikit-learn==1.4.1.post1"
        ]
    "#,
    )?;

    uv_snapshot!(context.filters(), context.tree().arg("--invert"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    joblib v1.3.2
    └── scikit-learn v1.4.1.post1
        └── project v0.1.0
    numpy v1.26.4
    ├── scikit-learn v1.4.1.post1 (*)
    └── scipy v1.12.0
        └── scikit-learn v1.4.1.post1 (*)
    threadpoolctl v3.4.0
    └── scikit-learn v1.4.1.post1 (*)
    (*) Package tree already displayed

    ----- stderr -----
    Resolved 6 packages in [TIME]
    "
    );

    uv_snapshot!(context.filters(), context.tree().arg("--invert").arg("--no-dedupe"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    joblib v1.3.2
    └── scikit-learn v1.4.1.post1
        └── project v0.1.0
    numpy v1.26.4
    ├── scikit-learn v1.4.1.post1
    │   └── project v0.1.0
    └── scipy v1.12.0
        └── scikit-learn v1.4.1.post1
            └── project v0.1.0
    threadpoolctl v3.4.0
    └── scikit-learn v1.4.1.post1
        └── project v0.1.0

    ----- stderr -----
    Resolved 6 packages in [TIME]
    "
    );

    Ok(())
}

#[test]
fn frozen() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = ["anyio"]
    "#,
    )?;

    uv_snapshot!(context.filters(), context.tree().arg("--universal"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    └── anyio v4.3.0
        ├── idna v3.6
        └── sniffio v1.3.1

    ----- stderr -----
    Resolved 4 packages in [TIME]
    "
    );

    // `uv tree` should update the lockfile
    let lock = context.read("uv.lock");
    assert!(!lock.is_empty());

    // Update the project dependencies.
    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = ["iniconfig"]
    "#,
    )?;

    // Running with `--frozen` should show the stale tree.
    uv_snapshot!(context.filters(), context.tree().arg("--frozen"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    └── anyio v4.3.0
        ├── idna v3.6
        └── sniffio v1.3.1

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

    Ok(())
}

#[test]
fn outdated() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = ["anyio==3.0.0"]
    "#,
    )?;

    uv_snapshot!(context.filters(), context.tree().arg("--outdated").arg("--universal"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    └── anyio v3.0.0 (latest: v4.3.0)
        ├── idna v3.6
        └── sniffio v1.3.1

    ----- stderr -----
    Resolved 4 packages in [TIME]
    "
    );

    Ok(())
}

/// Test that `uv tree --outdated` with a relative `exclude-newer` span recomputes the
/// cutoff timestamp relative to the current time, not the time the lock was generated.
///
/// Uses idna which has releases at:
/// - 3.6: 2023-11-25
/// - 3.7: 2024-04-11
#[test]
fn outdated_exclude_newer_relative() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = ["idna"]
    "#,
    )?;

    // Lock at 2024-05-01 with `--exclude-newer "3 weeks"`.
    // Cutoff: 2024-04-10 → resolves idna 3.6 (released 2023-11-25, before cutoff).
    // idna 3.7 (released 2024-04-11) is excluded.
    uv_snapshot!(context.filters(), context
        .lock()
        .env_remove(EnvVars::UV_EXCLUDE_NEWER)
        .env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2024-05-01T00:00:00Z")
        .arg("--exclude-newer")
        .arg("3 weeks"), @"
    success: true
    exit_code: 0
    ----- stdout -----

    ----- stderr -----
    Resolved 2 packages in [TIME]
    ");

    // Run `--outdated` at a later time (2024-06-01) with the same span.
    // The recomputed cutoff is 2024-05-11, which is after idna 3.7 (2024-04-11),
    // so idna 3.7 should be reported as the latest version.
    uv_snapshot!(context.filters(), context
        .tree()
        .arg("--outdated")
        .arg("--universal")
        .env_remove(EnvVars::UV_EXCLUDE_NEWER)
        .env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2024-06-01T00:00:00Z")
        .arg("--exclude-newer")
        .arg("3 weeks"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    └── idna v3.6 (latest: v3.7)

    ----- stderr -----
    Resolved 2 packages in [TIME]
    "
    );

    Ok(())
}

#[test]
fn platform_dependencies() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = [
            "black"
        ]
    "#,
    )?;

    // When `--universal` is _not_ provided, `colorama` should _not_ be included.
    #[cfg(not(windows))]
    uv_snapshot!(context.filters(), context.tree(), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    └── black v24.3.0
        ├── click v8.1.7
        ├── mypy-extensions v1.0.0
        ├── packaging v24.0
        ├── pathspec v0.12.1
        └── platformdirs v4.2.0

    ----- stderr -----
    Resolved 8 packages in [TIME]
    ");

    // Unless `--python-platform` is set to `windows`, in which case it should be included.
    uv_snapshot!(context.filters(), context.tree().arg("--python-platform").arg("windows"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    └── black v24.3.0
        ├── click v8.1.7
        │   └── colorama v0.4.6
        ├── mypy-extensions v1.0.0
        ├── packaging v24.0
        ├── pathspec v0.12.1
        └── platformdirs v4.2.0

    ----- stderr -----
    Resolved 8 packages in [TIME]
    ");

    // When `--universal` is _not_ provided, should include `colorama`, even though it's only
    // included on Windows.
    uv_snapshot!(context.filters(), context.tree().arg("--universal"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    └── black v24.3.0
        ├── click v8.1.7
        │   └── colorama v0.4.6
        ├── mypy-extensions v1.0.0
        ├── packaging v24.0
        ├── pathspec v0.12.1
        └── platformdirs v4.2.0

    ----- stderr -----
    Resolved 8 packages in [TIME]
    "
    );

    // `uv tree` should update the lockfile
    let lock = context.read("uv.lock");
    assert!(!lock.is_empty());

    Ok(())
}

#[test]
fn platform_dependencies_inverted() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = [
            "click"
        ]
    "#,
    )?;

    // When `--universal` is _not_ provided, `colorama` should _not_ be included.
    uv_snapshot!(context.filters(), context.tree().arg("--invert").arg("--python-platform").arg("linux"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    click v8.1.7
    └── project v0.1.0

    ----- stderr -----
    Resolved 3 packages in [TIME]
    ");

    // Unless `--python-platform` is set to `windows`, in which case it should be included.
    uv_snapshot!(context.filters(), context.tree().arg("--invert").arg("--python-platform").arg("windows"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    colorama v0.4.6
    └── click v8.1.7
        └── project v0.1.0

    ----- stderr -----
    Resolved 3 packages in [TIME]
    ");

    Ok(())
}

#[test]
fn repeated_dependencies() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = [
            "anyio < 2 ; sys_platform == 'win32'",
            "anyio > 2 ; sys_platform == 'linux'",
        ]
    "#,
    )?;

    // Should include both versions of `anyio`, which have different dependencies.
    uv_snapshot!(context.filters(), context.tree().arg("--universal"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    ├── anyio v1.4.0
    │   ├── async-generator v1.10
    │   ├── idna v3.6
    │   └── sniffio v1.3.1
    └── anyio v4.3.0
        ├── idna v3.6
        └── sniffio v1.3.1

    ----- stderr -----
    Resolved 6 packages in [TIME]
    "
    );

    // `uv tree` should update the lockfile
    let lock = context.read("uv.lock");
    assert!(!lock.is_empty());

    Ok(())
}

/// In this case, a package is included twice at the same version, but pointing to different direct
/// URLs.
#[test]
fn repeated_version() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let v1 = context.temp_dir.child("v1");
    fs_err::create_dir_all(&v1)?;
    let pyproject_toml = v1.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "dependency"
        version = "0.0.1"
        requires-python = ">=3.12"
        dependencies = ["anyio==3.7.0"]
        "#,
    )?;

    let v2 = context.temp_dir.child("v2");
    fs_err::create_dir_all(&v2)?;
    let pyproject_toml = v2.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "dependency"
        version = "0.0.1"
        requires-python = ">=3.12"
        dependencies = ["anyio==3.0.0"]
        "#,
    )?;

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(&formatdoc! {
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = [
          "dependency @ {} ; sys_platform == 'darwin'",
          "dependency @ {} ; sys_platform != 'darwin'",
        ]
        "#,
        Url::from_file_path(context.temp_dir.join("v1")).unwrap(),
        Url::from_file_path(context.temp_dir.join("v2")).unwrap(),
    })?;

    uv_snapshot!(context.filters(), context.tree().arg("--universal"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    ├── dependency v0.0.1
    │   └── anyio v3.7.0
    │       ├── idna v3.6
    │       └── sniffio v1.3.1
    └── dependency v0.0.1
        └── anyio v3.0.0
            ├── idna v3.6
            └── sniffio v1.3.1

    ----- stderr -----
    Resolved 7 packages in [TIME]
    "
    );

    // `uv tree` should update the lockfile
    let lock = context.read("uv.lock");
    assert!(!lock.is_empty());

    Ok(())
}

#[test]
fn dev_dependencies() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = ["iniconfig"]

        [tool.uv]
        dev-dependencies = ["anyio"]
    "#,
    )?;

    uv_snapshot!(context.filters(), context.tree(), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    ├── iniconfig v2.0.0
    └── anyio v4.3.0 (group: dev)
        ├── idna v3.6
        └── sniffio v1.3.1

    ----- stderr -----
    warning: The `tool.uv.dev-dependencies` field (used in `pyproject.toml`) is deprecated and will be removed in a future release; use `dependency-groups.dev` instead
    Resolved 5 packages in [TIME]
    "
    );

    uv_snapshot!(context.filters(), context.tree().arg("--no-dev"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    └── iniconfig v2.0.0

    ----- stderr -----
    warning: The `tool.uv.dev-dependencies` field (used in `pyproject.toml`) is deprecated and will be removed in a future release; use `dependency-groups.dev` instead
    Resolved 5 packages in [TIME]
    "
    );

    // `uv tree` should update the lockfile
    let lock = context.read("uv.lock");
    assert!(!lock.is_empty());

    Ok(())
}

#[test]
fn dev_dependencies_inverted() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = ["iniconfig"]

        [tool.uv]
        dev-dependencies = ["anyio"]
    "#,
    )?;

    uv_snapshot!(context.filters(), context.tree().arg("--universal").arg("--invert"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    idna v3.6
    └── anyio v4.3.0
        └── project v0.1.0 (group: dev)
    iniconfig v2.0.0
    └── project v0.1.0
    sniffio v1.3.1
    └── anyio v4.3.0 (*)
    (*) Package tree already displayed

    ----- stderr -----
    warning: The `tool.uv.dev-dependencies` field (used in `pyproject.toml`) is deprecated and will be removed in a future release; use `dependency-groups.dev` instead
    Resolved 5 packages in [TIME]
    "
    );

    uv_snapshot!(context.filters(), context.tree().arg("--universal").arg("--invert").arg("--no-dev"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    iniconfig v2.0.0
    └── project v0.1.0

    ----- stderr -----
    warning: The `tool.uv.dev-dependencies` field (used in `pyproject.toml`) is deprecated and will be removed in a future release; use `dependency-groups.dev` instead
    Resolved 5 packages in [TIME]
    "
    );

    // `uv tree` should update the lockfile
    let lock = context.read("uv.lock");
    assert!(!lock.is_empty());

    Ok(())
}

#[test]
fn optional_dependencies() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = ["iniconfig", "flask[dotenv]"]

        [project.optional-dependencies]
        async = ["anyio"]
    "#,
    )?;

    uv_snapshot!(context.filters(), context.tree().arg("--universal"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    ├── flask[dotenv] v3.0.2
    │   ├── blinker v1.7.0
    │   ├── click v8.1.7
    │   │   └── colorama v0.4.6
    │   ├── itsdangerous v2.1.2
    │   ├── jinja2 v3.1.3
    │   │   └── markupsafe v2.1.5
    │   ├── werkzeug v3.0.1
    │   │   └── markupsafe v2.1.5
    │   └── python-dotenv v1.0.1 (extra: dotenv)
    ├── iniconfig v2.0.0
    └── anyio v4.3.0 (extra: async)
        ├── idna v3.6
        └── sniffio v1.3.1

    ----- stderr -----
    Resolved 14 packages in [TIME]
    "
    );

    // `uv tree` should update the lockfile
    let lock = context.read("uv.lock");
    assert!(!lock.is_empty());

    Ok(())
}

#[test]
fn optional_dependencies_inverted() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = ["iniconfig", "flask[dotenv]"]

        [project.optional-dependencies]
        async = ["anyio"]
    "#,
    )?;

    uv_snapshot!(context.filters(), context.tree().arg("--universal").arg("--invert"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    blinker v1.7.0
    └── flask v3.0.2
        └── project[dotenv] v0.1.0
    colorama v0.4.6
    └── click v8.1.7
        └── flask v3.0.2 (*)
    idna v3.6
    └── anyio v4.3.0
        └── project v0.1.0 (extra: async)
    iniconfig v2.0.0
    └── project v0.1.0
    itsdangerous v2.1.2
    └── flask v3.0.2 (*)
    markupsafe v2.1.5
    ├── jinja2 v3.1.3
    │   └── flask v3.0.2 (*)
    └── werkzeug v3.0.1
        └── flask v3.0.2 (*)
    python-dotenv v1.0.1
    └── flask v3.0.2 (extra: dotenv) (*)
    sniffio v1.3.1
    └── anyio v4.3.0 (*)
    (*) Package tree already displayed

    ----- stderr -----
    Resolved 14 packages in [TIME]
    "
    );

    // `uv tree` should update the lockfile
    let lock = context.read("uv.lock");
    assert!(!lock.is_empty());

    Ok(())
}

/// Regression test for <https://github.com/astral-sh/uv/issues/19327>.
///
/// When a package is required both as a plain dep and as a dep with extras (e.g., from a
/// dependency group), `uv tree` should not display extra-conditional dependencies for the plain
/// occurrence.
#[test]
fn dep_and_group_extras() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = ["flask"]

        [dependency-groups]
        dev = ["flask[dotenv]"]
    "#,
    )?;

    // Plain `flask` should not show `python-dotenv` (which belongs to the `dotenv` extra),
    // but the `flask[dotenv]` occurrence should still be expanded in its own extra context.
    uv_snapshot!(context.filters(), context.tree().arg("--universal"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    ├── flask v3.0.2
    │   ├── blinker v1.7.0
    │   ├── click v8.1.7
    │   │   └── colorama v0.4.6
    │   ├── itsdangerous v2.1.2
    │   ├── jinja2 v3.1.3
    │   │   └── markupsafe v2.1.5
    │   └── werkzeug v3.0.1
    │       └── markupsafe v2.1.5
    └── flask[dotenv] v3.0.2 (group: dev)
        ├── blinker v1.7.0
        ├── click v8.1.7 (*)
        ├── itsdangerous v2.1.2
        ├── jinja2 v3.1.3 (*)
        ├── werkzeug v3.0.1 (*)
        └── python-dotenv v1.0.1 (extra: dotenv)
    (*) Package tree already displayed

    ----- stderr -----
    Resolved 10 packages in [TIME]
    "
    );

    // With `--no-dedupe`, `flask[dotenv]` is expanded and shows `python-dotenv` as an extra dep,
    // while plain `flask` still does not show it.
    uv_snapshot!(context.filters(), context.tree().arg("--universal").arg("--no-dedupe"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    ├── flask v3.0.2
    │   ├── blinker v1.7.0
    │   ├── click v8.1.7
    │   │   └── colorama v0.4.6
    │   ├── itsdangerous v2.1.2
    │   ├── jinja2 v3.1.3
    │   │   └── markupsafe v2.1.5
    │   └── werkzeug v3.0.1
    │       └── markupsafe v2.1.5
    └── flask[dotenv] v3.0.2 (group: dev)
        ├── blinker v1.7.0
        ├── click v8.1.7
        │   └── colorama v0.4.6
        ├── itsdangerous v2.1.2
        ├── jinja2 v3.1.3
        │   └── markupsafe v2.1.5
        ├── werkzeug v3.0.1
        │   └── markupsafe v2.1.5
        └── python-dotenv v1.0.1 (extra: dotenv)

    ----- stderr -----
    Resolved 10 packages in [TIME]
    "
    );

    Ok(())
}

#[test]
fn dep_and_group_extras_with_extra_only_dependency() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let leaf = context.temp_dir.child("leaf");
    fs_err::create_dir_all(leaf.path())?;
    leaf.child("pyproject.toml").write_str(
        r#"
        [project]
        name = "leaf"
        version = "0.1.0"
        requires-python = ">=3.12"
        "#,
    )?;
    let leaf_url = Url::from_file_path(leaf.path())
        .map_err(|()| anyhow::anyhow!("failed to convert leaf path to URL"))?;

    let child = context.temp_dir.child("child");
    fs_err::create_dir_all(child.path())?;
    child.child("pyproject.toml").write_str(&formatdoc! {
        r#"
        [project]
        name = "child"
        version = "0.1.0"
        requires-python = ">=3.12"

        [project.optional-dependencies]
        extra = ["leaf @ {}"]
        "#,
        leaf_url,
    })?;
    let child_url = Url::from_file_path(child.path())
        .map_err(|()| anyhow::anyhow!("failed to convert child path to URL"))?;

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(&formatdoc! {
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = ["child @ {}"]

        [dependency-groups]
        dev = ["child[extra] @ {}"]
        "#,
        child_url,
        child_url,
    })?;

    uv_snapshot!(context.filters(), context.tree(), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    ├── child v0.1.0
    └── child[extra] v0.1.0 (group: dev)
        └── leaf v0.1.0 (extra: extra)

    ----- stderr -----
    Resolved 3 packages in [TIME]
    ");

    Ok(())
}

#[test]
fn dep_and_group_extras_with_different_extras_in_path() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let leaf = context.temp_dir.child("leaf");
    fs_err::create_dir_all(leaf.path())?;
    leaf.child("pyproject.toml").write_str(
        r#"
        [project]
        name = "leaf"
        version = "0.1.0"
        requires-python = ">=3.12"
        "#,
    )?;
    let leaf_url = Url::from_file_path(leaf.path())
        .map_err(|()| anyhow::anyhow!("failed to convert leaf path to URL"))?;

    let a = context.temp_dir.child("a");
    fs_err::create_dir_all(a.path())?;
    let b = context.temp_dir.child("b");
    fs_err::create_dir_all(b.path())?;
    let b_url = Url::from_file_path(b.path())
        .map_err(|()| anyhow::anyhow!("failed to convert b path to URL"))?;
    a.child("pyproject.toml").write_str(&formatdoc! {
        r#"
        [project]
        name = "a"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = ["b[extra2] @ {}"]
        "#,
        b_url,
    })?;
    let a_url = Url::from_file_path(a.path())
        .map_err(|()| anyhow::anyhow!("failed to convert a path to URL"))?;

    b.child("pyproject.toml").write_str(&formatdoc! {
        r#"
        [project]
        name = "b"
        version = "0.1.0"
        requires-python = ">=3.12"

        [project.optional-dependencies]
        extra1 = ["a @ {}"]
        extra2 = ["leaf @ {}"]
        "#,
        a_url,
        leaf_url,
    })?;

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(&formatdoc! {
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = ["b[extra1] @ {}"]
        "#,
        b_url,
    })?;

    uv_snapshot!(context.filters(), context.tree(), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    └── b[extra1] v0.1.0
        └── a v0.1.0 (extra: extra1)
            └── b[extra2] v0.1.0
                └── leaf v0.1.0 (extra: extra2)

    ----- stderr -----
    Resolved 4 packages in [TIME]
    ");

    Ok(())
}

#[test]
fn package() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = ["scikit-learn==1.4.1.post1", "pandas"]
    "#,
    )?;

    uv_snapshot!(context.filters(), context.tree(), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    ├── pandas v2.2.1
    │   ├── numpy v1.26.4
    │   ├── python-dateutil v2.9.0.post0
    │   │   └── six v1.16.0
    │   ├── pytz v2024.1
    │   └── tzdata v2024.1
    └── scikit-learn v1.4.1.post1
        ├── joblib v1.3.2
        ├── numpy v1.26.4
        ├── scipy v1.12.0
        │   └── numpy v1.26.4
        └── threadpoolctl v3.4.0

    ----- stderr -----
    Resolved 11 packages in [TIME]
    "
    );

    uv_snapshot!(context.filters(), context.tree().arg("--package").arg("scipy"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    scipy v1.12.0
    └── numpy v1.26.4

    ----- stderr -----
    Resolved 11 packages in [TIME]
    "
    );

    uv_snapshot!(context.filters(), context.tree().arg("--package").arg("numpy").arg("--invert"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    numpy v1.26.4
    ├── pandas v2.2.1
    │   └── project v0.1.0
    ├── scikit-learn v1.4.1.post1
    │   └── project v0.1.0
    └── scipy v1.12.0
        └── scikit-learn v1.4.1.post1 (*)
    (*) Package tree already displayed

    ----- stderr -----
    Resolved 11 packages in [TIME]
    "
    );

    // `uv tree` should update the lockfile
    let lock = context.read("uv.lock");
    assert!(!lock.is_empty());

    Ok(())
}

#[test]
fn group() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = ["typing-extensions"]

        [dependency-groups]
        foo = ["anyio"]
        bar = ["iniconfig"]
        dev = ["sniffio"]
        "#,
    )?;

    context.lock().assert().success();

    uv_snapshot!(context.filters(), context.tree(), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    ├── typing-extensions v4.10.0
    └── sniffio v1.3.1 (group: dev)

    ----- stderr -----
    Resolved 6 packages in [TIME]
    ");

    uv_snapshot!(context.filters(), context.tree().arg("--only-group").arg("bar"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    └── iniconfig v2.0.0 (group: bar)

    ----- stderr -----
    Resolved 6 packages in [TIME]
    ");

    uv_snapshot!(context.filters(), context.tree().arg("--group").arg("foo"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    ├── typing-extensions v4.10.0
    ├── sniffio v1.3.1 (group: dev)
    └── anyio v4.3.0 (group: foo)
        ├── idna v3.6
        └── sniffio v1.3.1

    ----- stderr -----
    Resolved 6 packages in [TIME]
    ");

    uv_snapshot!(context.filters(), context.tree().arg("--group").arg("foo").arg("--group").arg("bar"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    ├── typing-extensions v4.10.0
    ├── iniconfig v2.0.0 (group: bar)
    ├── sniffio v1.3.1 (group: dev)
    └── anyio v4.3.0 (group: foo)
        ├── idna v3.6
        └── sniffio v1.3.1

    ----- stderr -----
    Resolved 6 packages in [TIME]
    ");

    uv_snapshot!(context.filters(), context.tree().arg("--all-groups"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    ├── typing-extensions v4.10.0
    ├── iniconfig v2.0.0 (group: bar)
    ├── sniffio v1.3.1 (group: dev)
    └── anyio v4.3.0 (group: foo)
        ├── idna v3.6
        └── sniffio v1.3.1

    ----- stderr -----
    Resolved 6 packages in [TIME]
    ");

    uv_snapshot!(context.filters(), context.tree().arg("--all-groups").arg("--no-group").arg("bar"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    ├── typing-extensions v4.10.0
    ├── sniffio v1.3.1 (group: dev)
    └── anyio v4.3.0 (group: foo)
        ├── idna v3.6
        └── sniffio v1.3.1

    ----- stderr -----
    Resolved 6 packages in [TIME]
    ");

    Ok(())
}

#[test]
fn cycle() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = ["testtools==2.3.0", "fixtures==3.0.0"]
    "#,
    )?;

    uv_snapshot!(context.filters(), context.tree().arg("--universal"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    ├── fixtures v3.0.0
    │   ├── pbr v6.0.0
    │   ├── six v1.16.0
    │   └── testtools v2.3.0
    │       ├── extras v1.0.0
    │       ├── fixtures v3.0.0 (*)
    │       ├── pbr v6.0.0
    │       ├── python-mimeparse v1.6.0
    │       ├── six v1.16.0
    │       ├── traceback2 v1.4.0
    │       │   └── linecache2 v1.0.0
    │       └── unittest2 v1.1.0
    │           ├── argparse v1.4.0
    │           ├── six v1.16.0
    │           └── traceback2 v1.4.0 (*)
    └── testtools v2.3.0 (*)
    (*) Package tree already displayed

    ----- stderr -----
    Resolved 11 packages in [TIME]
    "
    );

    uv_snapshot!(context.filters(), context.tree().arg("--package").arg("traceback2").arg("--package").arg("six"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    six v1.16.0
    traceback2 v1.4.0
    └── linecache2 v1.0.0

    ----- stderr -----
    Resolved 11 packages in [TIME]
    "
    );

    uv_snapshot!(context.filters(), context.tree().arg("--package").arg("traceback2").arg("--package").arg("six").arg("--invert"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    six v1.16.0
    ├── fixtures v3.0.0
    │   ├── project v0.1.0
    │   └── testtools v2.3.0
    │       ├── fixtures v3.0.0 (*)
    │       └── project v0.1.0
    ├── testtools v2.3.0 (*)
    └── unittest2 v1.1.0
        └── testtools v2.3.0 (*)
    traceback2 v1.4.0
    ├── testtools v2.3.0 (*)
    └── unittest2 v1.1.0 (*)
    (*) Package tree already displayed

    ----- stderr -----
    Resolved 11 packages in [TIME]
    "
    );

    // `uv tree` should update the lockfile
    let lock = context.read("uv.lock");
    assert!(!lock.is_empty());

    Ok(())
}

#[test]
fn cycle_no_orphaned_roots() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = ["testtools==2.3.0", "fixtures==3.0.0"]
    "#,
    )?;

    // With --depth 1, only "project" should appear as a root — transitive deps
    // involved in cycles (e.g. testtools <-> fixtures) must not be promoted to roots.
    uv_snapshot!(context.filters(), context.tree().arg("--universal").arg("--depth").arg("1"), @r###"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    ├── fixtures v3.0.0
    └── testtools v2.3.0

    ----- stderr -----
    Resolved 11 packages in [TIME]
    "###);

    Ok(())
}

#[test]
fn cycle_no_infinite_loop() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = ["testtools==2.3.0", "fixtures==3.0.0"]
    "#,
    )?;

    // This should complete without hanging, and cycles should be marked with (*)
    uv_snapshot!(context.filters(), context.tree().arg("--universal").arg("--depth").arg("2"), @r###"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    ├── fixtures v3.0.0
    │   ├── pbr v6.0.0
    │   ├── six v1.16.0
    │   └── testtools v2.3.0
    └── testtools v2.3.0
        ├── extras v1.0.0
        ├── fixtures v3.0.0 (*)
        ├── pbr v6.0.0
        ├── python-mimeparse v1.6.0
        ├── six v1.16.0
        ├── traceback2 v1.4.0
        └── unittest2 v1.1.0
    (*) Package tree already displayed

    ----- stderr -----
    Resolved 11 packages in [TIME]
    "###
    );

    Ok(())
}

#[test]
fn cycle_invert() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = ["testtools==2.3.0", "fixtures==3.0.0"]
    "#,
    )?;

    // With --invert, leaf packages should be roots and the tree should show
    // reverse dependencies without orphaned roots from cycle-breaking.
    uv_snapshot!(context.filters(), context.tree().arg("--universal").arg("--invert").arg("--depth").arg("1"), @r###"
    success: true
    exit_code: 0
    ----- stdout -----
    argparse v1.4.0
    └── unittest2 v1.1.0
    extras v1.0.0
    └── testtools v2.3.0
    linecache2 v1.0.0
    └── traceback2 v1.4.0
    pbr v6.0.0
    ├── fixtures v3.0.0
    └── testtools v2.3.0
    python-mimeparse v1.6.0
    └── testtools v2.3.0
    six v1.16.0
    ├── fixtures v3.0.0
    ├── testtools v2.3.0
    └── unittest2 v1.1.0

    ----- stderr -----
    Resolved 11 packages in [TIME]
    "###);

    Ok(())
}

#[test]
fn cycle_depth_boundary_no_premature_dedupe() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = ["testtools==2.3.0", "fixtures==3.0.0"]
    "#,
    )?;

    // With --depth 3, packages at the depth boundary (depth 3) are shown but not
    // marked as visited. Packages below the boundary (e.g., `fixtures` at depth 1)
    // are correctly marked visited and show (*) on later appearances. Leaf packages
    // like `pbr` (no children in this graph) appear without (*) even when visited,
    // since there is nothing to deduplicate.
    uv_snapshot!(context.filters(), context.tree().arg("--universal").arg("--depth").arg("3"), @r"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    ├── fixtures v3.0.0
    │   ├── pbr v6.0.0
    │   ├── six v1.16.0
    │   └── testtools v2.3.0
    │       ├── extras v1.0.0
    │       ├── fixtures v3.0.0 (*)
    │       ├── pbr v6.0.0
    │       ├── python-mimeparse v1.6.0
    │       ├── six v1.16.0
    │       ├── traceback2 v1.4.0
    │       └── unittest2 v1.1.0
    └── testtools v2.3.0 (*)
    (*) Package tree already displayed

    ----- stderr -----
    Resolved 11 packages in [TIME]
    ");

    Ok(())
}

#[test]
fn cycle_invert_deep() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = ["testtools==2.3.0", "fixtures==3.0.0"]
    "#,
    )?;

    // With --invert and --depth 2, cycles in the reversed graph should be
    // detected and marked with (*) without causing infinite loops.
    uv_snapshot!(context.filters(), context.tree().arg("--universal").arg("--invert").arg("--depth").arg("2"), @r"
    success: true
    exit_code: 0
    ----- stdout -----
    argparse v1.4.0
    └── unittest2 v1.1.0
        └── testtools v2.3.0
    extras v1.0.0
    └── testtools v2.3.0
        ├── fixtures v3.0.0
        └── project v0.1.0
    linecache2 v1.0.0
    └── traceback2 v1.4.0
        ├── testtools v2.3.0 (*)
        └── unittest2 v1.1.0 (*)
    pbr v6.0.0
    ├── fixtures v3.0.0
    │   ├── project v0.1.0
    │   └── testtools v2.3.0 (*)
    └── testtools v2.3.0 (*)
    python-mimeparse v1.6.0
    └── testtools v2.3.0 (*)
    six v1.16.0
    ├── fixtures v3.0.0 (*)
    ├── testtools v2.3.0 (*)
    └── unittest2 v1.1.0 (*)
    (*) Package tree already displayed

    ----- stderr -----
    Resolved 11 packages in [TIME]
    ");

    Ok(())
}

#[test]
fn cycle_depth_no_dedupe() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = ["testtools==2.3.0", "fixtures==3.0.0"]
    "#,
    )?;

    // With --no-dedupe and --depth 2, packages should be expanded each time they
    // appear (up to the depth limit), and cycles should still be marked with (*).
    uv_snapshot!(context.filters(), context.tree().arg("--universal").arg("--no-dedupe").arg("--depth").arg("2"), @r###"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    ├── fixtures v3.0.0
    │   ├── pbr v6.0.0
    │   ├── six v1.16.0
    │   └── testtools v2.3.0
    └── testtools v2.3.0
        ├── extras v1.0.0
        ├── fixtures v3.0.0
        ├── pbr v6.0.0
        ├── python-mimeparse v1.6.0
        ├── six v1.16.0
        ├── traceback2 v1.4.0
        └── unittest2 v1.1.0

    ----- stderr -----
    Resolved 11 packages in [TIME]
    "###);

    Ok(())
}

#[test]
fn workspace_dev() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = ["anyio"]

        [dependency-groups]
        dev = ["child"]

        [tool.uv.workspace]
        members = ["child"]

        [tool.uv.sources]
        child = { workspace = true }
    "#,
    )?;

    let child = context.temp_dir.child("child");
    let pyproject_toml = child.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "child"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = ["iniconfig"]
    "#,
    )?;

    uv_snapshot!(context.filters(), context.tree().arg("--universal"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    ├── anyio v4.3.0
    │   ├── idna v3.6
    │   └── sniffio v1.3.1
    └── child v0.1.0 (group: dev)
        └── iniconfig v2.0.0
    child v0.1.0 (*)
    (*) Package tree already displayed

    ----- stderr -----
    Resolved 6 packages in [TIME]
    "
    );

    // Under `--no-dev`, the member should still be included, since we show the entire workspace.
    // But it shouldn't be considered a dependency of the root.
    uv_snapshot!(context.filters(), context.tree().arg("--universal").arg("--no-dev"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    └── anyio v4.3.0
        ├── idna v3.6
        └── sniffio v1.3.1
    child v0.1.0
    └── iniconfig v2.0.0

    ----- stderr -----
    Resolved 6 packages in [TIME]
    "
    );

    // `uv tree` should update the lockfile
    let lock = context.read("uv.lock");
    assert!(!lock.is_empty());

    Ok(())
}

#[test]
fn non_project() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [tool.uv.workspace]
        members = []

        [dependency-groups]
        async = ["anyio"]
    "#,
    )?;

    uv_snapshot!(context.filters(), context.tree().arg("--universal"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    anyio v4.3.0 (group: async)
    ├── idna v3.6
    └── sniffio v1.3.1

    ----- stderr -----
    warning: No `requires-python` value found in the workspace. Defaulting to `>=3.12`.
    Resolved 3 packages in [TIME]
    "
    );

    // `uv tree` should update the lockfile
    let lock = context.read("uv.lock");
    assert!(!lock.is_empty());

    Ok(())
}

#[test]
fn non_project_member() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [tool.uv.workspace]
        members = ["child"]

        [dependency-groups]
        async = ["anyio"]
        "#,
    )?;

    let child = context.temp_dir.child("child");
    child.child("pyproject.toml").write_str(
        r#"
        [project]
        name = "child"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = ["iniconfig", "sniffio", "anyio"]

        [build-system]
        requires = ["uv_build>=0.7,<10000"]
        build-backend = "uv_build"
        "#,
    )?;

    uv_snapshot!(context.filters(), context.tree().arg("--universal"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    anyio v4.3.0 (group: async)
    ├── idna v3.6
    └── sniffio v1.3.1
    child v0.1.0
    ├── anyio v4.3.0 (*)
    ├── iniconfig v2.0.0
    └── sniffio v1.3.1
    (*) Package tree already displayed

    ----- stderr -----
    Resolved 5 packages in [TIME]
    "
    );

    uv_snapshot!(context.filters(), context.tree().arg("--universal").arg("--invert"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    idna v3.6
    └── anyio v4.3.0
        └── child v0.1.0
    iniconfig v2.0.0
    └── child v0.1.0
    sniffio v1.3.1
    ├── anyio v4.3.0 (*)
    └── child v0.1.0
    (*) Package tree already displayed

    ----- stderr -----
    Resolved 5 packages in [TIME]
    "
    );

    // `uv tree` should update the lockfile
    let lock = context.read("uv.lock");
    assert!(!lock.is_empty());

    Ok(())
}

#[test]
fn script() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let script = context.temp_dir.child("script.py");
    script.write_str(indoc! {r#"
        # /// script
        # requires-python = ">=3.11"
        # dependencies = [
        #   "requests<3",
        #   "rich",
        # ]
        # ///

        import requests
        from rich.pretty import pprint

        resp = requests.get("https://peps.python.org/api/peps.json")
        data = resp.json()
        pprint([(k, v["title"]) for k, v in data.items()][:10])
    "#})?;

    uv_snapshot!(context.filters(), context.tree().arg("--script").arg(script.path()), @"
    success: true
    exit_code: 0
    ----- stdout -----
    rich v13.7.1
    ├── markdown-it-py v3.0.0
    │   └── mdurl v0.1.2
    └── pygments v2.17.2
    requests v2.31.0
    ├── certifi v2024.2.2
    ├── charset-normalizer v3.3.2
    ├── idna v3.6
    └── urllib3 v2.2.1

    ----- stderr -----
    Resolved 9 packages in [TIME]
    ");

    // If the lockfile didn't exist already, it shouldn't be persisted to disk.
    assert!(!context.temp_dir.child("uv.lock").exists());

    // Explicitly lock the script.
    uv_snapshot!(context.filters(), context.lock().arg("--script").arg(script.path()), @"
    success: true
    exit_code: 0
    ----- stdout -----

    ----- stderr -----
    Resolved 9 packages in [TIME]
    ");

    let lock = context.read("script.py.lock");

    insta::with_settings!({
        filters => context.filters(),
    }, {
        assert_snapshot!(
            lock, @r#"
        version = 1
        revision = 3
        requires-python = ">=3.11"

        [options]
        exclude-newer = "2024-03-25T00:00:00Z"

        [manifest]
        requirements = [
            { name = "requests", specifier = "<3" },
            { name = "rich" },
        ]

        [[package]]
        name = "certifi"
        version = "2024.2.2"
        source = { registry = "https://pypi.org/simple" }
        sdist = { url = "https://files.pythonhosted.org/packages/71/da/e94e26401b62acd6d91df2b52954aceb7f561743aa5ccc32152886c76c96/certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", size = 164886, upload-time = "2024-02-02T01:22:17.364Z" }
        wheels = [
            { url = "https://files.pythonhosted.org/packages/ba/06/a07f096c664aeb9f01624f858c3add0a4e913d6c96257acb4fce61e7de14/certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1", size = 163774, upload-time = "2024-02-02T01:22:14.86Z" },
        ]

        [[package]]
        name = "charset-normalizer"
        version = "3.3.2"
        source = { registry = "https://pypi.org/simple" }
        sdist = { url = "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", size = 104809, upload-time = "2023-11-01T04:04:59.997Z" }
        wheels = [
            { url = "https://files.pythonhosted.org/packages/68/77/02839016f6fbbf808e8b38601df6e0e66c17bbab76dff4613f7511413597/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", size = 191647, upload-time = "2023-11-01T04:02:55.329Z" },
            { url = "https://files.pythonhosted.org/packages/3e/33/21a875a61057165e92227466e54ee076b73af1e21fe1b31f1e292251aa1e/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", size = 121434, upload-time = "2023-11-01T04:02:57.173Z" },
            { url = "https://files.pythonhosted.org/packages/dd/51/68b61b90b24ca35495956b718f35a9756ef7d3dd4b3c1508056fa98d1a1b/charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", size = 118979, upload-time = "2023-11-01T04:02:58.442Z" },
            { url = "https://files.pythonhosted.org/packages/e4/a6/7ee57823d46331ddc37dd00749c95b0edec2c79b15fc0d6e6efb532e89ac/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", size = 136582, upload-time = "2023-11-01T04:02:59.776Z" },
            { url = "https://files.pythonhosted.org/packages/74/f1/0d9fe69ac441467b737ba7f48c68241487df2f4522dd7246d9426e7c690e/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", size = 146645, upload-time = "2023-11-01T04:03:02.186Z" },
            { url = "https://files.pythonhosted.org/packages/05/31/e1f51c76db7be1d4aef220d29fbfa5dbb4a99165d9833dcbf166753b6dc0/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", size = 139398, upload-time = "2023-11-01T04:03:04.255Z" },
            { url = "https://files.pythonhosted.org/packages/40/26/f35951c45070edc957ba40a5b1db3cf60a9dbb1b350c2d5bef03e01e61de/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", size = 140273, upload-time = "2023-11-01T04:03:05.983Z" },
            { url = "https://files.pythonhosted.org/packages/07/07/7e554f2bbce3295e191f7e653ff15d55309a9ca40d0362fcdab36f01063c/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", size = 142577, upload-time = "2023-11-01T04:03:07.567Z" },
            { url = "https://files.pythonhosted.org/packages/d8/b5/eb705c313100defa57da79277d9207dc8d8e45931035862fa64b625bfead/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", size = 137747, upload-time = "2023-11-01T04:03:08.886Z" },
            { url = "https://files.pythonhosted.org/packages/19/28/573147271fd041d351b438a5665be8223f1dd92f273713cb882ddafe214c/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", size = 143375, upload-time = "2023-11-01T04:03:10.613Z" },
            { url = "https://files.pythonhosted.org/packages/cf/7c/f3b682fa053cc21373c9a839e6beba7705857075686a05c72e0f8c4980ca/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", size = 148474, upload-time = "2023-11-01T04:03:11.973Z" },
            { url = "https://files.pythonhosted.org/packages/1e/49/7ab74d4ac537ece3bc3334ee08645e231f39f7d6df6347b29a74b0537103/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", size = 140232, upload-time = "2023-11-01T04:03:13.505Z" },
            { url = "https://files.pythonhosted.org/packages/2d/dc/9dacba68c9ac0ae781d40e1a0c0058e26302ea0660e574ddf6797a0347f7/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", size = 140859, upload-time = "2023-11-01T04:03:17.362Z" },
            { url = "https://files.pythonhosted.org/packages/6c/c2/4a583f800c0708dd22096298e49f887b49d9746d0e78bfc1d7e29816614c/charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", size = 92509, upload-time = "2023-11-01T04:03:21.453Z" },
            { url = "https://files.pythonhosted.org/packages/57/ec/80c8d48ac8b1741d5b963797b7c0c869335619e13d4744ca2f67fc11c6fc/charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", size = 99870, upload-time = "2023-11-01T04:03:22.723Z" },
            { url = "https://files.pythonhosted.org/packages/d1/b2/fcedc8255ec42afee97f9e6f0145c734bbe104aac28300214593eb326f1d/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", size = 192892, upload-time = "2023-11-01T04:03:24.135Z" },
            { url = "https://files.pythonhosted.org/packages/2e/7d/2259318c202f3d17f3fe6438149b3b9e706d1070fe3fcbb28049730bb25c/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", size = 122213, upload-time = "2023-11-01T04:03:25.66Z" },
            { url = "https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", size = 119404, upload-time = "2023-11-01T04:03:27.04Z" },
            { url = "https://files.pythonhosted.org/packages/99/b0/9c365f6d79a9f0f3c379ddb40a256a67aa69c59609608fe7feb6235896e1/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", size = 137275, upload-time = "2023-11-01T04:03:28.466Z" },
            { url = "https://files.pythonhosted.org/packages/91/33/749df346e93d7a30cdcb90cbfdd41a06026317bfbfb62cd68307c1a3c543/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", size = 147518, upload-time = "2023-11-01T04:03:29.82Z" },
            { url = "https://files.pythonhosted.org/packages/72/1a/641d5c9f59e6af4c7b53da463d07600a695b9824e20849cb6eea8a627761/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", size = 140182, upload-time = "2023-11-01T04:03:31.511Z" },
            { url = "https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", size = 141869, upload-time = "2023-11-01T04:03:32.887Z" },
            { url = "https://files.pythonhosted.org/packages/df/3e/a06b18788ca2eb6695c9b22325b6fde7dde0f1d1838b1792a0076f58fe9d/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", size = 144042, upload-time = "2023-11-01T04:03:34.412Z" },
            { url = "https://files.pythonhosted.org/packages/45/59/3d27019d3b447a88fe7e7d004a1e04be220227760264cc41b405e863891b/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", size = 138275, upload-time = "2023-11-01T04:03:35.759Z" },
            { url = "https://files.pythonhosted.org/packages/7b/ef/5eb105530b4da8ae37d506ccfa25057961b7b63d581def6f99165ea89c7e/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", size = 144819, upload-time = "2023-11-01T04:03:37.216Z" },
            { url = "https://files.pythonhosted.org/packages/a2/51/e5023f937d7f307c948ed3e5c29c4b7a3e42ed2ee0b8cdf8f3a706089bf0/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", size = 149415, upload-time = "2023-11-01T04:03:38.694Z" },
            { url = "https://files.pythonhosted.org/packages/24/9d/2e3ef673dfd5be0154b20363c5cdcc5606f35666544381bee15af3778239/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", size = 141212, upload-time = "2023-11-01T04:03:40.07Z" },
            { url = "https://files.pythonhosted.org/packages/5b/ae/ce2c12fcac59cb3860b2e2d76dc405253a4475436b1861d95fe75bdea520/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", size = 142167, upload-time = "2023-11-01T04:03:41.491Z" },
            { url = "https://files.pythonhosted.org/packages/ed/3a/a448bf035dce5da359daf9ae8a16b8a39623cc395a2ffb1620aa1bce62b0/charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", size = 93041, upload-time = "2023-11-01T04:03:42.836Z" },
            { url = "https://files.pythonhosted.org/packages/b6/7c/8debebb4f90174074b827c63242c23851bdf00a532489fba57fef3416e40/charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", size = 100397, upload-time = "2023-11-01T04:03:44.467Z" },
            { url = "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", size = 48543, upload-time = "2023-11-01T04:04:58.622Z" },
        ]

        [[package]]
        name = "idna"
        version = "3.6"
        source = { registry = "https://pypi.org/simple" }
        sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426, upload-time = "2023-11-25T15:40:54.902Z" }
        wheels = [
            { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567, upload-time = "2023-11-25T15:40:52.604Z" },
        ]

        [[package]]
        name = "markdown-it-py"
        version = "3.0.0"
        source = { registry = "https://pypi.org/simple" }
        dependencies = [
            { name = "mdurl" },
        ]
        sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" }
        wheels = [
            { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
        ]

        [[package]]
        name = "mdurl"
        version = "0.1.2"
        source = { registry = "https://pypi.org/simple" }
        sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
        wheels = [
            { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
        ]

        [[package]]
        name = "pygments"
        version = "2.17.2"
        source = { registry = "https://pypi.org/simple" }
        sdist = { url = "https://files.pythonhosted.org/packages/55/59/8bccf4157baf25e4aa5a0bb7fa3ba8600907de105ebc22b0c78cfbf6f565/pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367", size = 4827772, upload-time = "2023-11-21T20:43:53.875Z" }
        wheels = [
            { url = "https://files.pythonhosted.org/packages/97/9c/372fef8377a6e340b1704768d20daaded98bf13282b5327beb2e2fe2c7ef/pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", size = 1179756, upload-time = "2023-11-21T20:43:49.423Z" },
        ]

        [[package]]
        name = "requests"
        version = "2.31.0"
        source = { registry = "https://pypi.org/simple" }
        dependencies = [
            { name = "certifi" },
            { name = "charset-normalizer" },
            { name = "idna" },
            { name = "urllib3" },
        ]
        sdist = { url = "https://files.pythonhosted.org/packages/9d/be/10918a2eac4ae9f02f6cfe6414b7a155ccd8f7f9d4380d62fd5b955065c3/requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1", size = 110794, upload-time = "2023-05-22T15:12:44.175Z" }
        wheels = [
            { url = "https://files.pythonhosted.org/packages/70/8e/0e2d847013cb52cd35b38c009bb167a1a26b2ce6cd6965bf26b47bc0bf44/requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", size = 62574, upload-time = "2023-05-22T15:12:42.313Z" },
        ]

        [[package]]
        name = "rich"
        version = "13.7.1"
        source = { registry = "https://pypi.org/simple" }
        dependencies = [
            { name = "markdown-it-py" },
            { name = "pygments" },
        ]
        sdist = { url = "https://files.pythonhosted.org/packages/b3/01/c954e134dc440ab5f96952fe52b4fdc64225530320a910473c1fe270d9aa/rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432", size = 221248, upload-time = "2024-02-28T14:51:19.472Z" }
        wheels = [
            { url = "https://files.pythonhosted.org/packages/87/67/a37f6214d0e9fe57f6ae54b2956d550ca8365857f42a1ce0392bb21d9410/rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222", size = 240681, upload-time = "2024-02-28T14:51:14.353Z" },
        ]

        [[package]]
        name = "urllib3"
        version = "2.2.1"
        source = { registry = "https://pypi.org/simple" }
        sdist = { url = "https://files.pythonhosted.org/packages/7a/50/7fd50a27caa0652cd4caf224aa87741ea41d3265ad13f010886167cfcc79/urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19", size = 291020, upload-time = "2024-02-18T03:55:57.539Z" }
        wheels = [
            { url = "https://files.pythonhosted.org/packages/a2/73/a68704750a7679d0b6d3ad7aa8d4da8e14e151ae82e6fee774e6e0d05ec8/urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", size = 121067, upload-time = "2024-02-18T03:55:54.704Z" },
        ]
        "#
        );
    });

    // Update the dependencies.
    script.write_str(indoc! {r#"
        # /// script
        # requires-python = ">=3.11"
        # dependencies = [
        #   "iniconfig",
        #   "requests<3",
        #   "rich",
        # ]
        # ///

        import requests
        from rich.pretty import pprint

        resp = requests.get("https://peps.python.org/api/peps.json")
        data = resp.json()
        pprint([(k, v["title"]) for k, v in data.items()][:10])
    "#})?;

    // `uv tree` should update the lockfile.
    uv_snapshot!(context.filters(), context.tree().arg("--script").arg(script.path()), @"
    success: true
    exit_code: 0
    ----- stdout -----
    rich v13.7.1
    ├── markdown-it-py v3.0.0
    │   └── mdurl v0.1.2
    └── pygments v2.17.2
    requests v2.31.0
    ├── certifi v2024.2.2
    ├── charset-normalizer v3.3.2
    ├── idna v3.6
    └── urllib3 v2.2.1
    iniconfig v2.0.0

    ----- stderr -----
    Resolved 10 packages in [TIME]
    ");

    let lock = context.read("script.py.lock");

    insta::with_settings!({
        filters => context.filters(),
    }, {
        assert_snapshot!(
            lock, @r#"
        version = 1
        revision = 3
        requires-python = ">=3.11"

        [options]
        exclude-newer = "2024-03-25T00:00:00Z"

        [manifest]
        requirements = [
            { name = "iniconfig" },
            { name = "requests", specifier = "<3" },
            { name = "rich" },
        ]

        [[package]]
        name = "certifi"
        version = "2024.2.2"
        source = { registry = "https://pypi.org/simple" }
        sdist = { url = "https://files.pythonhosted.org/packages/71/da/e94e26401b62acd6d91df2b52954aceb7f561743aa5ccc32152886c76c96/certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", size = 164886, upload-time = "2024-02-02T01:22:17.364Z" }
        wheels = [
            { url = "https://files.pythonhosted.org/packages/ba/06/a07f096c664aeb9f01624f858c3add0a4e913d6c96257acb4fce61e7de14/certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1", size = 163774, upload-time = "2024-02-02T01:22:14.86Z" },
        ]

        [[package]]
        name = "charset-normalizer"
        version = "3.3.2"
        source = { registry = "https://pypi.org/simple" }
        sdist = { url = "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", size = 104809, upload-time = "2023-11-01T04:04:59.997Z" }
        wheels = [
            { url = "https://files.pythonhosted.org/packages/68/77/02839016f6fbbf808e8b38601df6e0e66c17bbab76dff4613f7511413597/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", size = 191647, upload-time = "2023-11-01T04:02:55.329Z" },
            { url = "https://files.pythonhosted.org/packages/3e/33/21a875a61057165e92227466e54ee076b73af1e21fe1b31f1e292251aa1e/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", size = 121434, upload-time = "2023-11-01T04:02:57.173Z" },
            { url = "https://files.pythonhosted.org/packages/dd/51/68b61b90b24ca35495956b718f35a9756ef7d3dd4b3c1508056fa98d1a1b/charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", size = 118979, upload-time = "2023-11-01T04:02:58.442Z" },
            { url = "https://files.pythonhosted.org/packages/e4/a6/7ee57823d46331ddc37dd00749c95b0edec2c79b15fc0d6e6efb532e89ac/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", size = 136582, upload-time = "2023-11-01T04:02:59.776Z" },
            { url = "https://files.pythonhosted.org/packages/74/f1/0d9fe69ac441467b737ba7f48c68241487df2f4522dd7246d9426e7c690e/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", size = 146645, upload-time = "2023-11-01T04:03:02.186Z" },
            { url = "https://files.pythonhosted.org/packages/05/31/e1f51c76db7be1d4aef220d29fbfa5dbb4a99165d9833dcbf166753b6dc0/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", size = 139398, upload-time = "2023-11-01T04:03:04.255Z" },
            { url = "https://files.pythonhosted.org/packages/40/26/f35951c45070edc957ba40a5b1db3cf60a9dbb1b350c2d5bef03e01e61de/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", size = 140273, upload-time = "2023-11-01T04:03:05.983Z" },
            { url = "https://files.pythonhosted.org/packages/07/07/7e554f2bbce3295e191f7e653ff15d55309a9ca40d0362fcdab36f01063c/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", size = 142577, upload-time = "2023-11-01T04:03:07.567Z" },
            { url = "https://files.pythonhosted.org/packages/d8/b5/eb705c313100defa57da79277d9207dc8d8e45931035862fa64b625bfead/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", size = 137747, upload-time = "2023-11-01T04:03:08.886Z" },
            { url = "https://files.pythonhosted.org/packages/19/28/573147271fd041d351b438a5665be8223f1dd92f273713cb882ddafe214c/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", size = 143375, upload-time = "2023-11-01T04:03:10.613Z" },
            { url = "https://files.pythonhosted.org/packages/cf/7c/f3b682fa053cc21373c9a839e6beba7705857075686a05c72e0f8c4980ca/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", size = 148474, upload-time = "2023-11-01T04:03:11.973Z" },
            { url = "https://files.pythonhosted.org/packages/1e/49/7ab74d4ac537ece3bc3334ee08645e231f39f7d6df6347b29a74b0537103/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", size = 140232, upload-time = "2023-11-01T04:03:13.505Z" },
            { url = "https://files.pythonhosted.org/packages/2d/dc/9dacba68c9ac0ae781d40e1a0c0058e26302ea0660e574ddf6797a0347f7/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", size = 140859, upload-time = "2023-11-01T04:03:17.362Z" },
            { url = "https://files.pythonhosted.org/packages/6c/c2/4a583f800c0708dd22096298e49f887b49d9746d0e78bfc1d7e29816614c/charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", size = 92509, upload-time = "2023-11-01T04:03:21.453Z" },
            { url = "https://files.pythonhosted.org/packages/57/ec/80c8d48ac8b1741d5b963797b7c0c869335619e13d4744ca2f67fc11c6fc/charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", size = 99870, upload-time = "2023-11-01T04:03:22.723Z" },
            { url = "https://files.pythonhosted.org/packages/d1/b2/fcedc8255ec42afee97f9e6f0145c734bbe104aac28300214593eb326f1d/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", size = 192892, upload-time = "2023-11-01T04:03:24.135Z" },
            { url = "https://files.pythonhosted.org/packages/2e/7d/2259318c202f3d17f3fe6438149b3b9e706d1070fe3fcbb28049730bb25c/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", size = 122213, upload-time = "2023-11-01T04:03:25.66Z" },
            { url = "https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", size = 119404, upload-time = "2023-11-01T04:03:27.04Z" },
            { url = "https://files.pythonhosted.org/packages/99/b0/9c365f6d79a9f0f3c379ddb40a256a67aa69c59609608fe7feb6235896e1/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", size = 137275, upload-time = "2023-11-01T04:03:28.466Z" },
            { url = "https://files.pythonhosted.org/packages/91/33/749df346e93d7a30cdcb90cbfdd41a06026317bfbfb62cd68307c1a3c543/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", size = 147518, upload-time = "2023-11-01T04:03:29.82Z" },
            { url = "https://files.pythonhosted.org/packages/72/1a/641d5c9f59e6af4c7b53da463d07600a695b9824e20849cb6eea8a627761/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", size = 140182, upload-time = "2023-11-01T04:03:31.511Z" },
            { url = "https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", size = 141869, upload-time = "2023-11-01T04:03:32.887Z" },
            { url = "https://files.pythonhosted.org/packages/df/3e/a06b18788ca2eb6695c9b22325b6fde7dde0f1d1838b1792a0076f58fe9d/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", size = 144042, upload-time = "2023-11-01T04:03:34.412Z" },
            { url = "https://files.pythonhosted.org/packages/45/59/3d27019d3b447a88fe7e7d004a1e04be220227760264cc41b405e863891b/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", size = 138275, upload-time = "2023-11-01T04:03:35.759Z" },
            { url = "https://files.pythonhosted.org/packages/7b/ef/5eb105530b4da8ae37d506ccfa25057961b7b63d581def6f99165ea89c7e/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", size = 144819, upload-time = "2023-11-01T04:03:37.216Z" },
            { url = "https://files.pythonhosted.org/packages/a2/51/e5023f937d7f307c948ed3e5c29c4b7a3e42ed2ee0b8cdf8f3a706089bf0/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", size = 149415, upload-time = "2023-11-01T04:03:38.694Z" },
            { url = "https://files.pythonhosted.org/packages/24/9d/2e3ef673dfd5be0154b20363c5cdcc5606f35666544381bee15af3778239/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", size = 141212, upload-time = "2023-11-01T04:03:40.07Z" },
            { url = "https://files.pythonhosted.org/packages/5b/ae/ce2c12fcac59cb3860b2e2d76dc405253a4475436b1861d95fe75bdea520/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", size = 142167, upload-time = "2023-11-01T04:03:41.491Z" },
            { url = "https://files.pythonhosted.org/packages/ed/3a/a448bf035dce5da359daf9ae8a16b8a39623cc395a2ffb1620aa1bce62b0/charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", size = 93041, upload-time = "2023-11-01T04:03:42.836Z" },
            { url = "https://files.pythonhosted.org/packages/b6/7c/8debebb4f90174074b827c63242c23851bdf00a532489fba57fef3416e40/charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", size = 100397, upload-time = "2023-11-01T04:03:44.467Z" },
            { url = "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", size = 48543, upload-time = "2023-11-01T04:04:58.622Z" },
        ]

        [[package]]
        name = "idna"
        version = "3.6"
        source = { registry = "https://pypi.org/simple" }
        sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426, upload-time = "2023-11-25T15:40:54.902Z" }
        wheels = [
            { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567, upload-time = "2023-11-25T15:40:52.604Z" },
        ]

        [[package]]
        name = "iniconfig"
        version = "2.0.0"
        source = { registry = "https://pypi.org/simple" }
        sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" }
        wheels = [
            { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" },
        ]

        [[package]]
        name = "markdown-it-py"
        version = "3.0.0"
        source = { registry = "https://pypi.org/simple" }
        dependencies = [
            { name = "mdurl" },
        ]
        sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" }
        wheels = [
            { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
        ]

        [[package]]
        name = "mdurl"
        version = "0.1.2"
        source = { registry = "https://pypi.org/simple" }
        sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
        wheels = [
            { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
        ]

        [[package]]
        name = "pygments"
        version = "2.17.2"
        source = { registry = "https://pypi.org/simple" }
        sdist = { url = "https://files.pythonhosted.org/packages/55/59/8bccf4157baf25e4aa5a0bb7fa3ba8600907de105ebc22b0c78cfbf6f565/pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367", size = 4827772, upload-time = "2023-11-21T20:43:53.875Z" }
        wheels = [
            { url = "https://files.pythonhosted.org/packages/97/9c/372fef8377a6e340b1704768d20daaded98bf13282b5327beb2e2fe2c7ef/pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", size = 1179756, upload-time = "2023-11-21T20:43:49.423Z" },
        ]

        [[package]]
        name = "requests"
        version = "2.31.0"
        source = { registry = "https://pypi.org/simple" }
        dependencies = [
            { name = "certifi" },
            { name = "charset-normalizer" },
            { name = "idna" },
            { name = "urllib3" },
        ]
        sdist = { url = "https://files.pythonhosted.org/packages/9d/be/10918a2eac4ae9f02f6cfe6414b7a155ccd8f7f9d4380d62fd5b955065c3/requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1", size = 110794, upload-time = "2023-05-22T15:12:44.175Z" }
        wheels = [
            { url = "https://files.pythonhosted.org/packages/70/8e/0e2d847013cb52cd35b38c009bb167a1a26b2ce6cd6965bf26b47bc0bf44/requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", size = 62574, upload-time = "2023-05-22T15:12:42.313Z" },
        ]

        [[package]]
        name = "rich"
        version = "13.7.1"
        source = { registry = "https://pypi.org/simple" }
        dependencies = [
            { name = "markdown-it-py" },
            { name = "pygments" },
        ]
        sdist = { url = "https://files.pythonhosted.org/packages/b3/01/c954e134dc440ab5f96952fe52b4fdc64225530320a910473c1fe270d9aa/rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432", size = 221248, upload-time = "2024-02-28T14:51:19.472Z" }
        wheels = [
            { url = "https://files.pythonhosted.org/packages/87/67/a37f6214d0e9fe57f6ae54b2956d550ca8365857f42a1ce0392bb21d9410/rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222", size = 240681, upload-time = "2024-02-28T14:51:14.353Z" },
        ]

        [[package]]
        name = "urllib3"
        version = "2.2.1"
        source = { registry = "https://pypi.org/simple" }
        sdist = { url = "https://files.pythonhosted.org/packages/7a/50/7fd50a27caa0652cd4caf224aa87741ea41d3265ad13f010886167cfcc79/urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19", size = 291020, upload-time = "2024-02-18T03:55:57.539Z" }
        wheels = [
            { url = "https://files.pythonhosted.org/packages/a2/73/a68704750a7679d0b6d3ad7aa8d4da8e14e151ae82e6fee774e6e0d05ec8/urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", size = 121067, upload-time = "2024-02-18T03:55:54.704Z" },
        ]
        "#
        );
    });

    Ok(())
}

#[test]
fn only_group() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = [
            "iniconfig",
            "pip",
        ]

        [dependency-groups]
        dev = [
            "plotly",
            "pip",
        ]
        test = [
            "pytest",
        ]
    "#,
    )?;

    uv_snapshot!(context.filters(), context.tree().arg("--universal"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    ├── iniconfig v2.0.0
    ├── pip v24.0
    ├── pip v24.0 (group: dev)
    └── plotly v5.20.0 (group: dev)
        ├── packaging v24.0
        └── tenacity v8.2.3

    ----- stderr -----
    Resolved 9 packages in [TIME]
    "
    );

    uv_snapshot!(context.filters(), context.tree().arg("--universal").arg("--only-group").arg("dev"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    ├── pip v24.0 (group: dev)
    └── plotly v5.20.0 (group: dev)
        ├── packaging v24.0
        └── tenacity v8.2.3

    ----- stderr -----
    Resolved 9 packages in [TIME]
    "
    );

    uv_snapshot!(context.filters(), context.tree().arg("--universal").arg("--only-group").arg("test"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    └── pytest v8.1.1 (group: test)
        ├── colorama v0.4.6
        ├── iniconfig v2.0.0
        ├── packaging v24.0
        └── pluggy v1.4.0

    ----- stderr -----
    Resolved 9 packages in [TIME]
    "
    );

    // `uv tree` should update the lockfile
    let lock = context.read("uv.lock");
    assert!(!lock.is_empty());

    Ok(())
}

#[test]
fn show_sizes() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [project]
        name = "project"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = ["iniconfig"]
    "#,
    )?;

    uv_snapshot!(context.filters(), context.tree().arg("--show-sizes").arg("--universal"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    project v0.1.0
    └── iniconfig v2.0.0 ([SIZE])

    ----- stderr -----
    Resolved 2 packages in [TIME]
    "
    );

    Ok(())
}

#[test]
fn workspace_circular_dependencies() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    // Create workspace root
    let pyproject_toml = context.temp_dir.child("pyproject.toml");
    pyproject_toml.write_str(
        r#"
        [tool.uv.workspace]
        members = ["packages/*"]
    "#,
    )?;

    // Create package-a that depends on package-b
    let package_a_dir = context.temp_dir.child("packages").child("package-a");
    package_a_dir.create_dir_all()?;
    let package_a_pyproject = package_a_dir.child("pyproject.toml");
    package_a_pyproject.write_str(
        r#"
        [project]
        name = "package-a"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = ["package-b"]

        [tool.uv.sources]
        package-b = { workspace = true }
    "#,
    )?;

    // Create package-b that depends on package-a (circular dependency)
    let package_b_dir = context.temp_dir.child("packages").child("package-b");
    package_b_dir.create_dir_all()?;
    let package_b_pyproject = package_b_dir.child("pyproject.toml");
    package_b_pyproject.write_str(
        r#"
        [project]
        name = "package-b"
        version = "0.1.0"
        requires-python = ">=3.12"
        dependencies = ["package-a"]

        [tool.uv.sources]
        package-a = { workspace = true }
    "#,
    )?;

    // Test that package-a is at the root when requested
    uv_snapshot!(context.filters(), context.tree().arg("--package").arg("package-a"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    package-a v0.1.0
    └── package-b v0.1.0
        └── package-a v0.1.0 (*)
    (*) Package tree already displayed

    ----- stderr -----
    Resolved 2 packages in [TIME]
    "
    );

    // Test that package-b is at the root when requested
    uv_snapshot!(context.filters(), context.tree().arg("--package").arg("package-b"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    package-b v0.1.0
    └── package-a v0.1.0
        └── package-b v0.1.0 (*)
    (*) Package tree already displayed

    ----- stderr -----
    Resolved 2 packages in [TIME]
    "
    );

    // Test that both packages are shown as roots when both are requested
    uv_snapshot!(context.filters(), context.tree().arg("--package").arg("package-a").arg("--package").arg("package-b"), @"
    success: true
    exit_code: 0
    ----- stdout -----
    package-a v0.1.0
    └── package-b v0.1.0
        └── package-a v0.1.0 (*)
    package-b v0.1.0 (*)
    (*) Package tree already displayed

    ----- stderr -----
    Resolved 2 packages in [TIME]
    "
    );

    Ok(())
}
