use anyhow::Result;
use assert_fs::prelude::*;
use predicates::prelude::predicate;
use std::process::Command;

#[cfg(unix)]
use uv_fs::create_symlink;
use uv_test::{get_bin, uv_snapshot};

/// When the active cache directory is inside an explicit build source, we should warn and continue
/// the build.
#[test]
fn build_warns_cache_inside_source() -> Result<()> {
    let mut context = uv_test::test_context!("3.12");
    let project = context.temp_dir.child("project");

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

        [build-system]
        requires = ["uv_build>=0.5.15,<10000"]
        build-backend = "uv_build"
        "#,
    )?;
    project.child("src/project/__init__.py").touch()?;

    context.cache_dir = project.child(".uv-cache");

    uv_snapshot!(context.filters(), context.build().arg("--sdist").arg("project"), @"
    exit_code: 0 (success)
    ----- stderr -----
    warning: The cache directory `project/.uv-cache` is inside the build source directory `project` and may be included in distributions
    Building source distribution (uv build backend)...
    Successfully built project/dist/project-0.1.0.tar.gz
    ");

    project
        .child("dist/project-0.1.0.tar.gz")
        .assert(predicate::path::is_file());

    Ok(())
}

/// When the canonical cache directory is inside an explicit build source, we should warn even if
/// the configured cache path itself is outside the source.
#[test]
#[cfg(unix)]
fn build_warns_symlinked_cache_inside_source() -> Result<()> {
    let context = uv_test::test_context!("3.12");
    let project = context.temp_dir.child("project");

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

        [build-system]
        requires = ["uv_build>=0.5.15,<10000"]
        build-backend = "uv_build"
        "#,
    )?;
    project.child("src/project/__init__.py").touch()?;

    let cache_dir = project.child(".uv-cache");
    cache_dir.create_dir_all()?;
    let cache_link = context.temp_dir.child("cache-link");
    create_symlink(cache_dir.path(), cache_link.path())?;

    let mut command = Command::new(get_bin!());
    command
        .arg("build")
        .arg("--sdist")
        .arg("project")
        .arg("--cache-dir")
        .arg(cache_link.path());
    context.add_shared_env(&mut command, false);
    command.current_dir(context.temp_dir.path());

    uv_snapshot!(context.filters(), command, @"
    exit_code: 0 (success)
    ----- stderr -----
    warning: The cache directory `cache-link` is inside the build source directory `project` and may be included in distributions
    Building source distribution (uv build backend)...
    Successfully built project/dist/project-0.1.0.tar.gz
    ");

    project
        .child("dist/project-0.1.0.tar.gz")
        .assert(predicate::path::is_file());

    Ok(())
}

/// A cache in the workspace root is allowed when building a member that does not contain it.
#[test]
fn build_allows_cache_outside_selected_source() -> Result<()> {
    let mut context = uv_test::test_context!("3.12");
    let workspace = context.temp_dir.child("workspace");
    let member = workspace.child("member");

    workspace.child("pyproject.toml").write_str(
        r#"
        [tool.uv.workspace]
        members = ["member"]
        "#,
    )?;
    member.child("pyproject.toml").write_str(
        r#"
        [project]
        name = "member"
        version = "0.1.0"
        requires-python = ">=3.12"

        [build-system]
        requires = ["uv_build>=0.5.15,<10000"]
        build-backend = "uv_build"
        "#,
    )?;
    member.child("src/member/__init__.py").touch()?;

    context.cache_dir = workspace.child(".uv-cache");

    uv_snapshot!(context.filters(), context.build()
        .arg("--sdist")
        .arg("--package")
        .arg("member")
        .current_dir(&workspace), @"
    exit_code: 0 (success)
    ----- stderr -----
    Building source distribution (uv build backend)...
    Successfully built dist/member-0.1.0.tar.gz
    ");

    workspace
        .child("dist/member-0.1.0.tar.gz")
        .assert(predicate::path::is_file());

    Ok(())
}

/// When the project directory defaults to a current directory inside the cache directory, we should
/// error before using the cache.
#[test]
fn cache_current_dir_inside_cache() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    uv_snapshot!(context.filters(), context.command()
        .arg("cache")
        .arg("dir")
        .current_dir(context.cache_dir.path()), @"
    exit_code: 2 (failure)
    ----- stderr -----
    error: The project directory `.` is inside the cache directory `.`
    ");

    let child = context.cache_dir.child("child");
    child.create_dir_all()?;

    uv_snapshot!(context.filters(), context.command()
        .arg("cache")
        .arg("dir")
        .current_dir(child.path()), @"
    exit_code: 2 (failure)
    ----- stderr -----
    error: The project directory `.` is inside the cache directory `[CACHE_DIR]/`
    ");

    Ok(())
}

/// When the project directory is inside a symlinked cache directory, we should error before using
/// the cache.
#[test]
#[cfg(unix)]
fn cache_current_dir_inside_symlinked_cache() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let cache_link = context.temp_dir.child("cache-link");
    create_symlink(context.cache_dir.path(), cache_link.path())?;

    let child = context.cache_dir.child("child");
    child.create_dir_all()?;

    let mut command = Command::new(get_bin!());
    command
        .arg("cache")
        .arg("dir")
        .arg("--cache-dir")
        .arg(cache_link.path());
    context.add_shared_env(&mut command, false);
    command.current_dir(child.path());

    uv_snapshot!(context.filters(), command, @"
    exit_code: 2 (failure)
    ----- stderr -----
    error: The project directory `.` is inside the cache directory `[CACHE_DIR]/`
    ");

    Ok(())
}

/// When a workspace is inside the cache directory, we should error before locking the workspace.
#[test]
fn cache_workspace_inside_cache() -> Result<()> {
    let context = uv_test::test_context!("3.12");

    let workspace = context.cache_dir.child("workspace");
    workspace.child("pyproject.toml").write_str(
        r#"
        [tool.uv.workspace]
        members = ["member"]
        "#,
    )?;
    workspace
        .child("member")
        .child("pyproject.toml")
        .write_str(
            r#"
        [project]
        name = "member"
        version = "0.1.0"
        requires-python = ">=3.12"
        "#,
        )?;

    uv_snapshot!(context.filters(), context.lock()
        .arg("--project")
        .arg(workspace.path()), @"
    exit_code: 2 (failure)
    ----- stderr -----
    error: The project directory `[CACHE_DIR]/workspace` is inside the cache directory `[CACHE_DIR]/`
    ");

    Ok(())
}

/// When the cache directory is a non-canonical parent of the project directory, we should still
/// detect that the project is inside the cache.
#[test]
fn cache_project_inside_relative_parent_cache() -> Result<()> {
    let context = uv_test::test_context!("3.12");

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

    let mut command = Command::new(get_bin!());
    command.arg("lock").arg("--cache-dir").arg("..");
    context.add_shared_env(&mut command, false);
    command.current_dir(project.path());

    uv_snapshot!(context.filters(), command, @"
    exit_code: 2 (failure)
    ----- stderr -----
    error: The project directory `.` is inside the cache directory `[TEMP_DIR]/`
    ");

    Ok(())
}

/// When `--no-cache` is enabled, running from a project inside the configured cache directory
/// should not trip the persistent cache guard.
#[test]
fn cache_project_inside_cache_no_cache() -> Result<()> {
    let context = uv_test::test_context!("3.12");

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

    uv_snapshot!(context.filters(), context.lock().arg("--no-cache").current_dir(&project), @"
    exit_code: 0 (success)
    ----- stderr -----
    Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
    Resolved 1 package in [TIME]
    ");

    Ok(())
}

/// When the cache directory cannot be created (e.g., due to permissions), we should show a
/// chained error message that indicates we failed to initialize the cache.
#[test]
#[cfg(unix)]
fn cache_init_failure() -> Result<()> {
    use uv_test::ReadOnlyDirectoryGuard;

    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"]
        "#,
    )?;

    // Create a read-only directory that will serve as the parent of the cache.
    // The guard sets it to read-only and restores original permissions on drop (including panic).
    let cache_parent = context.temp_dir.child("cache_parent");
    fs_err::create_dir(&cache_parent)?;
    let _guard = ReadOnlyDirectoryGuard::new(cache_parent.path())?;

    // Point the cache to a subdirectory within the read-only parent
    let cache_dir = cache_parent.child("cache");

    // Filter both the relative path (in the first line) and absolute path (in the cause)
    let context = context
        .with_filter((r"cache_parent/cache", "[CACHE_DIR]"))
        .with_filter((
            r"failed to create directory `.*`",
            "failed to create directory `[CACHE_DIR]`",
        ));

    // Build the sync command manually to use our custom cache directory.
    // We can't use context.sync() because it adds --cache-dir with the default cache.
    let mut command = Command::new(get_bin!());
    command
        .arg("sync")
        .arg("--cache-dir")
        .arg(cache_dir.path())
        .current_dir(context.temp_dir.path());
    context.add_shared_env(&mut command, false);

    // Running a command should fail with a chained error about cache initialization
    uv_snapshot!(context.filters(), command, @"
    exit_code: 2 (failure)
    ----- stderr -----
    error: Failed to initialize cache at `[CACHE_DIR]`
      Caused by: failed to create directory `[CACHE_DIR]`: Permission denied (os error 13)
    ");

    Ok(())
}
