use llman::tool::command::RmUselessDirsArgs;
use llman::tool::rm_empty_dirs::run;
use std::fs;
use tempfile::TempDir;

#[test]
fn test_rm_useless_dirs_dry_run_and_live() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let root = temp_dir.path();

    fs::create_dir_all(root.join("a/b")).expect("Failed to create a/b");
    fs::create_dir_all(root.join("c/d")).expect("Failed to create c/d");
    fs::write(root.join("keep.txt"), "keep").expect("Failed to create keep.txt");

    let dry_run_args = RmUselessDirsArgs {
        config: None,
        path: Some(root.to_path_buf()),
        yes: false,
        gitignore: None,
        prune_ignored: false,
        verbose: false,
    };

    run(&dry_run_args).expect("Dry run failed");

    assert!(root.join("a/b").exists());
    assert!(root.join("c/d").exists());

    let live_args = RmUselessDirsArgs {
        config: None,
        path: Some(root.to_path_buf()),
        yes: true,
        gitignore: None,
        prune_ignored: false,
        verbose: false,
    };

    run(&live_args).expect("Live run failed");

    assert!(!root.join("a").exists());
    assert!(!root.join("c").exists());
    assert!(root.join("keep.txt").exists());
    assert!(root.exists());
}

#[test]
fn test_rm_useless_dirs_respects_gitignore() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let root = temp_dir.path();

    fs::create_dir_all(root.join("ignored_dir/inner")).expect("Failed to create ignored_dir/inner");
    fs::create_dir_all(root.join("remove_me/inner")).expect("Failed to create remove_me/inner");
    fs::write(root.join("keep.txt"), "keep").expect("Failed to create keep.txt");
    fs::write(root.join(".gitignore"), "ignored_dir/\n").expect("Failed to create .gitignore");

    let live_args = RmUselessDirsArgs {
        config: None,
        path: Some(root.to_path_buf()),
        yes: true,
        gitignore: Some(root.join(".gitignore")),
        prune_ignored: false,
        verbose: false,
    };

    run(&live_args).expect("Live run failed");

    assert!(root.join("ignored_dir").exists());
    assert!(!root.join("remove_me").exists());
    assert!(root.join("keep.txt").exists());
}

#[test]
fn test_rm_useless_dirs_prune_ignored() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let root = temp_dir.path();

    fs::create_dir_all(root.join("__pycache__")).expect("Failed to create __pycache__");
    fs::write(root.join("__pycache__/a.pyc"), "pyc").expect("Failed to create pyc");
    fs::create_dir_all(root.join("logs")).expect("Failed to create logs");
    fs::write(root.join("logs/app.log"), "log").expect("Failed to create log");
    fs::write(root.join("keep.txt"), "keep").expect("Failed to create keep.txt");
    fs::write(root.join(".gitignore"), "__pycache__/\n*.log\n")
        .expect("Failed to create .gitignore");

    let live_args = RmUselessDirsArgs {
        config: None,
        path: Some(root.to_path_buf()),
        yes: true,
        gitignore: Some(root.join(".gitignore")),
        prune_ignored: true,
        verbose: false,
    };

    run(&live_args).expect("Live run failed");

    assert!(!root.join("__pycache__").exists());
    assert!(!root.join("logs").exists());
    assert!(root.join("keep.txt").exists());
}

#[test]
fn test_rm_useless_dirs_protects_node_modules() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let root = temp_dir.path();

    fs::create_dir_all(root.join("node_modules/pkg")).expect("Failed to create node_modules");
    fs::write(root.join("node_modules/pkg/index.js"), "console.log('x');")
        .expect("Failed to create file");
    fs::create_dir_all(root.join(".cargo")).expect("Failed to create .cargo");
    fs::write(root.join(".cargo/config.toml"), "[build]\n")
        .expect("Failed to create .cargo config");
    fs::create_dir_all(root.join(".npm")).expect("Failed to create .npm");
    fs::write(root.join(".npm/_cache"), "cache").expect("Failed to create .npm file");
    fs::write(root.join(".gitignore"), "node_modules/\n").expect("Failed to create .gitignore");

    let live_args = RmUselessDirsArgs {
        config: None,
        path: Some(root.to_path_buf()),
        yes: true,
        gitignore: Some(root.join(".gitignore")),
        prune_ignored: true,
        verbose: false,
    };

    run(&live_args).expect("Live run failed");

    assert!(root.join("node_modules").exists());
    assert!(root.join(".cargo").exists());
    assert!(root.join(".npm").exists());
}

#[test]
fn test_rm_useless_dirs_removes_python_caches() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let root = temp_dir.path();

    fs::create_dir_all(root.join(".pytest_cache")).expect("Failed to create .pytest_cache");
    fs::write(root.join(".pytest_cache/data"), "data").expect("Failed to create data");
    fs::create_dir_all(root.join(".mypy_cache")).expect("Failed to create .mypy_cache");
    fs::write(root.join(".mypy_cache/data"), "data").expect("Failed to create data");
    fs::create_dir_all(root.join(".ruff_cache")).expect("Failed to create .ruff_cache");
    fs::write(root.join(".ruff_cache/data"), "data").expect("Failed to create data");
    fs::create_dir_all(root.join(".basedpyright")).expect("Failed to create .basedpyright");
    fs::write(root.join(".basedpyright/data"), "data").expect("Failed to create data");
    fs::create_dir_all(root.join(".pytype")).expect("Failed to create .pytype");
    fs::write(root.join(".pytype/data"), "data").expect("Failed to create data");
    fs::create_dir_all(root.join(".pyre")).expect("Failed to create .pyre");
    fs::write(root.join(".pyre/data"), "data").expect("Failed to create data");
    fs::create_dir_all(root.join(".ty")).expect("Failed to create .ty");
    fs::write(root.join(".ty/data"), "data").expect("Failed to create data");
    fs::create_dir_all(root.join(".ty_cache")).expect("Failed to create .ty_cache");
    fs::write(root.join(".ty_cache/data"), "data").expect("Failed to create data");
    fs::create_dir_all(root.join(".ty-cache")).expect("Failed to create .ty-cache");
    fs::write(root.join(".ty-cache/data"), "data").expect("Failed to create data");
    fs::write(root.join("keep.txt"), "keep").expect("Failed to create keep.txt");

    let live_args = RmUselessDirsArgs {
        config: None,
        path: Some(root.to_path_buf()),
        yes: true,
        gitignore: None,
        prune_ignored: false,
        verbose: false,
    };

    run(&live_args).expect("Live run failed");

    assert!(!root.join(".pytest_cache").exists());
    assert!(!root.join(".mypy_cache").exists());
    assert!(!root.join(".ruff_cache").exists());
    assert!(!root.join(".basedpyright").exists());
    assert!(!root.join(".pytype").exists());
    assert!(!root.join(".pyre").exists());
    assert!(!root.join(".ty").exists());
    assert!(!root.join(".ty_cache").exists());
    assert!(!root.join(".ty-cache").exists());
    assert!(root.join("keep.txt").exists());
}

#[test]
fn test_rm_useless_dirs_config_override_disables_protection() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let root = temp_dir.path();

    fs::create_dir_all(root.join("node_modules/pkg")).expect("Failed to create node_modules");
    fs::write(root.join("node_modules/pkg/index.js"), "console.log('x');")
        .expect("Failed to create file");
    fs::write(root.join(".gitignore"), "node_modules/\n").expect("Failed to create .gitignore");

    let config_path = root.join("config.yaml");
    fs::write(
        &config_path,
        r#"version: "0.1"
tools:
  rm-useless-dirs:
    protected:
      mode: override
      names: []
"#,
    )
    .expect("Failed to create config");

    let live_args = RmUselessDirsArgs {
        config: Some(config_path),
        path: Some(root.to_path_buf()),
        yes: true,
        gitignore: Some(root.join(".gitignore")),
        prune_ignored: true,
        verbose: false,
    };

    run(&live_args).expect("Live run failed");

    assert!(!root.join("node_modules").exists());
}

#[test]
fn test_rm_useless_dirs_config_extend_useless_list() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let root = temp_dir.path();

    fs::create_dir_all(root.join("custom-cache")).expect("Failed to create custom-cache");
    fs::write(root.join("custom-cache/data"), "data").expect("Failed to create data");

    let config_path = root.join("config.yaml");
    fs::write(
        &config_path,
        r#"version: "0.1"
tools:
  rm-useless-dirs:
    useless:
      mode: extend
      names: ["custom-cache"]
"#,
    )
    .expect("Failed to create config");

    let live_args = RmUselessDirsArgs {
        config: Some(config_path),
        path: Some(root.to_path_buf()),
        yes: true,
        gitignore: None,
        prune_ignored: false,
        verbose: false,
    };

    run(&live_args).expect("Live run failed");

    assert!(!root.join("custom-cache").exists());
}

#[test]
fn test_rm_useless_dirs_rejects_legacy_config_key() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let root = temp_dir.path();

    let config_path = root.join("config.yaml");
    fs::write(
        &config_path,
        r#"version: "0.1"
tools:
  rm-empty-dirs:
    useless:
      mode: extend
      names: ["legacy"]
"#,
    )
    .expect("Failed to create config");

    let live_args = RmUselessDirsArgs {
        config: Some(config_path),
        path: Some(root.to_path_buf()),
        yes: true,
        gitignore: None,
        prune_ignored: false,
        verbose: false,
    };

    let err = run(&live_args).expect_err("Expected legacy config to fail");
    assert!(err.to_string().contains("rm-empty-dirs"));
}

#[test]
fn test_rm_useless_dirs_default_gitignore_is_relative_to_target() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let root = temp_dir.path();

    let ignored_dir = "llman_rm_empty_dirs_ignored_dir_should_stay";
    fs::create_dir_all(root.join(ignored_dir).join("inner"))
        .expect("Failed to create ignored_dir/inner");
    fs::create_dir_all(root.join("remove_me/inner")).expect("Failed to create remove_me/inner");
    fs::write(root.join("keep.txt"), "keep").expect("Failed to create keep.txt");
    fs::write(root.join(".gitignore"), format!("{ignored_dir}/\n"))
        .expect("Failed to create .gitignore");

    let live_args = RmUselessDirsArgs {
        config: None,
        path: Some(root.to_path_buf()),
        yes: true,
        gitignore: None,
        prune_ignored: false,
        verbose: false,
    };

    run(&live_args).expect("Live run failed");

    assert!(root.join(ignored_dir).exists());
    assert!(!root.join("remove_me").exists());
    assert!(root.join("keep.txt").exists());
}

#[test]
fn test_rm_useless_dirs_skips_targets_in_protected_subtree() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let root = temp_dir.path();

    let protected_root = root.join("some/.git/objects");
    fs::create_dir_all(protected_root.join("pack")).expect("Failed to create pack dir");

    let live_args = RmUselessDirsArgs {
        config: None,
        path: Some(protected_root),
        yes: true,
        gitignore: None,
        prune_ignored: false,
        verbose: false,
    };

    run(&live_args).expect("Live run failed");

    assert!(root.join("some/.git/objects/pack").exists());
}

#[test]
fn test_rm_useless_dirs_prune_ignored_python_module_rename_cleanup() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let root = temp_dir.path();

    fs::create_dir_all(root.join(".git")).expect("Failed to create .git dir");
    fs::write(root.join(".gitignore"), "__pycache__/\n*.py[cod]\n")
        .expect("Failed to create .gitignore");

    fs::create_dir_all(root.join("src/new_pkg")).expect("Failed to create src/new_pkg");
    fs::write(root.join("src/new_pkg/__init__.py"), "from .api import x\n")
        .expect("Failed to create __init__.py");

    fs::create_dir_all(root.join("src/old_pkg/__pycache__"))
        .expect("Failed to create src/old_pkg/__pycache__");
    fs::write(
        root.join("src/old_pkg/__pycache__/old_pkg.cpython-313.pyc"),
        "pyc",
    )
    .expect("Failed to create ignored pyc");
    fs::write(root.join("src/old_pkg/orphan.pyc"), "pyc").expect("Failed to create orphan.pyc");

    let live_args = RmUselessDirsArgs {
        config: None,
        path: Some(root.join("src")),
        yes: true,
        gitignore: None,
        prune_ignored: true,
        verbose: false,
    };

    run(&live_args).expect("Live run failed");

    assert!(!root.join("src/old_pkg").exists());
    assert!(root.join("src/new_pkg").exists());
    assert!(root.join("src/new_pkg/__init__.py").exists());
    assert!(root.join("src").exists());
}

#[test]
fn test_rm_useless_dirs_prunes_parent_dir_with_only_pycache() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let root = temp_dir.path();

    fs::create_dir_all(root.join("src/pkg_only_cache/__pycache__"))
        .expect("Failed to create src/pkg_only_cache/__pycache__");
    fs::write(
        root.join("src/pkg_only_cache/__pycache__/pkg.cpython-313.pyc"),
        "pyc",
    )
    .expect("Failed to create ignored pyc");

    fs::create_dir_all(root.join("src/pkg_keep/__pycache__"))
        .expect("Failed to create src/pkg_keep/__pycache__");
    fs::write(root.join("src/pkg_keep/__init__.py"), "# pkg").expect("Failed to create __init__");
    fs::write(
        root.join("src/pkg_keep/__pycache__/pkg_keep.cpython-313.pyc"),
        "pyc",
    )
    .expect("Failed to create pyc");

    let live_args = RmUselessDirsArgs {
        config: None,
        path: Some(root.join("src")),
        yes: true,
        gitignore: None,
        prune_ignored: false,
        verbose: false,
    };

    run(&live_args).expect("Live run failed");

    assert!(!root.join("src/pkg_only_cache").exists());
    assert!(root.join("src/pkg_keep").exists());
    assert!(root.join("src/pkg_keep/__init__.py").exists());
}

#[test]
fn test_rm_useless_dirs_prunes_parent_dirs_with_only_other_useless_caches() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let root = temp_dir.path();

    let cases = [
        ("pytest_cache", ".pytest_cache"),
        ("mypy_cache", ".mypy_cache"),
        ("ruff_cache", ".ruff_cache"),
        ("basedpyright", ".basedpyright"),
        ("pytype", ".pytype"),
        ("pyre", ".pyre"),
        ("ty", ".ty"),
        ("ty_cache", ".ty_cache"),
        ("ty_cache_dash", ".ty-cache"),
    ];

    for (token, useless_dir) in cases {
        fs::create_dir_all(
            root.join("src")
                .join(format!("pkg_only_{token}"))
                .join(useless_dir),
        )
        .expect("Failed to create useless-only package dir");
        fs::write(
            root.join("src")
                .join(format!("pkg_only_{token}"))
                .join(useless_dir)
                .join("data"),
            "data",
        )
        .expect("Failed to create data file");

        fs::create_dir_all(
            root.join("src")
                .join(format!("pkg_keep_{token}"))
                .join(useless_dir),
        )
        .expect("Failed to create keep package dir");
        fs::write(
            root.join("src")
                .join(format!("pkg_keep_{token}"))
                .join("__init__.py"),
            "# pkg",
        )
        .expect("Failed to create __init__.py");
        fs::write(
            root.join("src")
                .join(format!("pkg_keep_{token}"))
                .join(useless_dir)
                .join("data"),
            "data",
        )
        .expect("Failed to create data file");
    }

    let live_args = RmUselessDirsArgs {
        config: None,
        path: Some(root.join("src")),
        yes: true,
        gitignore: None,
        prune_ignored: false,
        verbose: false,
    };

    run(&live_args).expect("Live run failed");

    for (token, _) in cases {
        assert!(!root.join("src").join(format!("pkg_only_{token}")).exists());
        assert!(root.join("src").join(format!("pkg_keep_{token}")).exists());
        assert!(
            root.join("src")
                .join(format!("pkg_keep_{token}"))
                .join("__init__.py")
                .exists()
        );
    }
}
