use biome_rowan::TextSize;
use salsa::Setter;

use crate::tests::test_db::file_path;
use crate::tests::test_db::library_root;
use crate::tests::test_db::workspace_root;
use crate::tests::test_db::TestDb;
use crate::DbInputs;
use crate::File;
use crate::FileRevision;
use crate::ImportLayer;
use crate::Package;

fn make_file(db: &mut TestDb, path: &str, contents: &str) -> File {
    File::new(
        db,
        file_path(path),
        FileRevision::zero(),
        Some(contents.to_string()),
        None,
    )
}

fn make_package_file(db: &mut TestDb, path: &str, contents: &str, package: Package) -> File {
    File::new(
        db,
        file_path(path),
        FileRevision::zero(),
        Some(contents.to_string()),
        Some(package),
    )
}

/// Register a set of installed packages on `LibraryRoots`, one library
/// root per package. Returns the packages in input order.
fn install_packages(db: &mut TestDb, names: &[&str]) -> Vec<Package> {
    let mut roots = Vec::new();
    let mut packages = Vec::new();
    for &name in names {
        let root = library_root(db, &format!("libs/{name}"));
        let pkg = Package::new(
            db,
            file_path(&format!("libs/{name}/DESCRIPTION")),
            name.to_string(),
            FileRevision::zero(),
            FileRevision::zero(),
            None,
            Vec::new(),
            Vec::new(),
        );
        root.set_packages(db).to(vec![pkg]);
        roots.push(root);
        packages.push(pkg);
    }
    db.library_roots().set_roots(db).to(roots);
    packages
}

/// Create a workspace package under a fresh `workspace/{name}` root and
/// register the root on `WorkspaceRoots`. Returns the package.
fn install_workspace_package(db: &mut TestDb, name: &str) -> Package {
    let root = workspace_root(db, &format!("workspace/{name}"));
    let pkg = Package::new(
        db,
        file_path(&format!("workspace/{name}/DESCRIPTION")),
        name.to_string(),
        FileRevision::zero(),
        FileRevision::zero(),
        None,
        Vec::new(),
        Vec::new(),
    );
    root.set_packages(db).to(vec![pkg]);
    db.workspace_roots().set_roots(db).to(vec![root]);
    pkg
}

fn attached_packages(layers: &[ImportLayer]) -> Vec<Package> {
    layers
        .iter()
        .filter_map(|layer| match layer {
            ImportLayer::Package(p) => Some(*p),
            _ => None,
        })
        .collect()
}

fn package_files(layers: &[ImportLayer]) -> Vec<File> {
    layers
        .iter()
        .filter_map(|layer| match layer {
            ImportLayer::File(f) => Some(*f),
            _ => None,
        })
        .collect()
}

#[test]
fn test_script_cursor_before_any_attach_sees_no_attached_packages() {
    let mut db = TestDb::new();
    let packages = install_packages(&mut db, &["dplyr", "ggplot2"]);
    let _ = packages;

    let source = "library(dplyr)\nlibrary(ggplot2)\nx <- 1\n";
    let file = make_file(&mut db, "/a.R", source);

    let layers = file.imports_at(&db, TextSize::from(0));
    assert_eq!(attached_packages(&layers), Vec::<Package>::new());
}

#[test]
fn test_script_cursor_after_all_attaches_sees_all_in_lifo_order() {
    let mut db = TestDb::new();
    let packages = install_packages(&mut db, &["dplyr", "ggplot2"]);
    let dplyr = packages[0];
    let ggplot2 = packages[1];

    let source = "library(dplyr)\nlibrary(ggplot2)\nx <- 1\n";
    let file = make_file(&mut db, "/a.R", source);

    let offset = TextSize::from(source.len() as u32);
    let layers = file.imports_at(&db, offset);
    // LIFO: latest `library()` call comes first.
    assert_eq!(attached_packages(&layers), vec![ggplot2, dplyr]);
}

#[test]
fn test_script_cursor_between_attaches_sees_only_earlier_ones() {
    let mut db = TestDb::new();
    let packages = install_packages(&mut db, &["dplyr", "ggplot2"]);
    let dplyr = packages[0];

    let source = "library(dplyr)\nlibrary(ggplot2)\nx <- 1\n";
    let file = make_file(&mut db, "/a.R", source);

    let offset = TextSize::from(source.find("library(ggplot2)").unwrap() as u32);
    let layers = file.imports_at(&db, offset);
    assert_eq!(attached_packages(&layers), vec![dplyr]);
}

#[test]
fn test_function_body_sees_file_scope_attaches_even_if_after_function_in_source() {
    // R's runtime. File-scope `library()` calls run before any function
    // body executes, so the function sees the package regardless of source
    // order. The offset filter must override its "before cursor" rule for
    // file-scope attaches when the cursor is inside a function body.
    let mut db = TestDb::new();
    let packages = install_packages(&mut db, &["dplyr"]);
    let dplyr = packages[0];

    let source = "f <- function() {\n  x\n}\nlibrary(dplyr)\n";
    let file = make_file(&mut db, "/a.R", source);

    let offset = TextSize::from(source.find("x\n}").unwrap() as u32);
    let layers = file.imports_at(&db, offset);
    assert!(attached_packages(&layers).contains(&dplyr));
}

#[test]
fn test_package_top_level_sees_predecessor_files_only() {
    let mut db = TestDb::new();
    let _ = install_packages(&mut db, &["base"]);
    let pkg = install_workspace_package(&mut db, "pkg");

    let a = make_package_file(&mut db, "/w/pkg/R/a.R", "first <- 1\n", pkg);
    let b_source = "x <- 1\n";
    let b = make_package_file(&mut db, "/w/pkg/R/b.R", b_source, pkg);
    let c = make_package_file(&mut db, "/w/pkg/R/c.R", "second <- 2\n", pkg);
    pkg.set_files(&mut db).to(vec![a, b, c]);

    // Cursor at top-level in b. Only a (the predecessor in `Package.files`)
    // is visible.
    let offset = TextSize::from(b_source.find('x').unwrap() as u32);
    let layers = b.imports_at(&db, offset);
    assert_eq!(package_files(&layers), vec![a]);
}

#[test]
fn test_package_function_body_sees_other_package_files_in_lifo_order() {
    let mut db = TestDb::new();
    let _ = install_packages(&mut db, &["base"]);
    let pkg = install_workspace_package(&mut db, "pkg");

    let a = make_package_file(&mut db, "/w/pkg/R/a.R", "first <- 1\n", pkg);
    let b_source = "f <- function() {\n  x\n}\n";
    let b = make_package_file(&mut db, "/w/pkg/R/b.R", b_source, pkg);
    let c = make_package_file(&mut db, "/w/pkg/R/c.R", "second <- 2\n", pkg);
    pkg.set_files(&mut db).to(vec![a, b, c]);

    // Cursor inside f's body. Full lazy view (same as `imports()`).
    // Other package files appear in LIFO order. Self (b) is excluded
    // since its own top-level bindings live in `exports`.
    let offset = TextSize::from(b_source.find("x\n}").unwrap() as u32);
    let layers = b.imports_at(&db, offset);
    assert_eq!(package_files(&layers), vec![c, a]);
}

#[test]
fn test_package_top_level_predecessors_appear_in_lifo_order() {
    // Multiple predecessors of the cursor's file appear latest-first
    // (LIFO), matching R's namespace where the most recently sourced
    // file's bindings win.
    let mut db = TestDb::new();
    let _ = install_packages(&mut db, &["base"]);
    let pkg = install_workspace_package(&mut db, "pkg");

    let a = make_package_file(&mut db, "/w/pkg/R/a.R", "first <- 1\n", pkg);
    let b = make_package_file(&mut db, "/w/pkg/R/b.R", "second <- 2\n", pkg);
    let c_source = "x <- 1\n";
    let c = make_package_file(&mut db, "/w/pkg/R/c.R", c_source, pkg);
    pkg.set_files(&mut db).to(vec![a, b, c]);

    let offset = TextSize::from(c_source.find('x').unwrap() as u32);
    let layers = c.imports_at(&db, offset);
    // Predecessors of c are [a, b] in declaration order. LIFO gives [b, a].
    assert_eq!(package_files(&layers), vec![b, a]);
}

#[test]
fn test_package_namespace_and_base_layers_always_visible() {
    let mut db = TestDb::new();
    let packages = install_packages(&mut db, &["base"]);
    let base = packages[0];
    let pkg = install_workspace_package(&mut db, "pkg");

    let file = make_package_file(&mut db, "/w/pkg/R/a.R", "x <- 1\n", pkg);
    pkg.set_files(&mut db).to(vec![file]);

    let layers = file.imports_at(&db, TextSize::from(0));
    assert!(attached_packages(&layers).contains(&base));
}

#[test]
fn test_package_script_top_level_resolves_as_standalone_script() {
    // A `data-raw/` file carries a package back-pointer but lives in
    // `scripts`, not `files`. At top level it must narrow like a standalone
    // script and never take the package path, which would log the spurious
    // "back-pointer but not in its files" warning from #1270.
    let mut db = TestDb::new();
    let dplyr = install_packages(&mut db, &["dplyr"])[0];
    let pkg = install_workspace_package(&mut db, "pkg");

    let r_file = make_package_file(&mut db, "/workspace/pkg/R/a.R", "internal <- 1\n", pkg);
    let source = "library(dplyr)\nx <- 1\n";
    let data_raw = make_package_file(&mut db, "/workspace/pkg/data-raw/prep.R", source, pkg);
    pkg.set_files(&mut db).to(vec![r_file]);
    pkg.set_scripts(&mut db).to(vec![data_raw]);

    // Before the `library()` call: nothing attached, and no `R/` `File`
    // layers (the script isn't part of the package's namespace).
    let before = data_raw.imports_at(&db, TextSize::from(0));
    assert_eq!(attached_packages(&before), Vec::<Package>::new());
    assert_eq!(package_files(&before), Vec::<File>::new());

    // After the call: dplyr attached, still no `R/` `File` layers.
    let after = data_raw.imports_at(&db, TextSize::from(source.len() as u32));
    assert_eq!(attached_packages(&after), vec![dplyr]);
    assert_eq!(package_files(&after), Vec::<File>::new());
}

#[test]
fn test_testthat_top_level_library_narrows_by_offset() {
    // A test file's own top-level `library()` call narrows like a script's:
    // invisible before the call, visible after. Helpers, the package, and
    // testthat (omitted here) stay visible at any offset.
    let mut db = TestDb::new();
    let cli = install_packages(&mut db, &["cli", "testthat", "base"])[0];
    let pkg = install_workspace_package(&mut db, "pkg");

    let source = "library(cli)\ntest_that('x', expect_true(TRUE))\n";
    let test_file = make_package_file(
        &mut db,
        "workspace/pkg/tests/testthat/test-x.R",
        source,
        pkg,
    );
    pkg.set_scripts(&mut db).to(vec![test_file]);

    let before = test_file.imports_at(&db, TextSize::from(0));
    assert!(!attached_packages(&before).contains(&cli));

    let after = test_file.imports_at(&db, TextSize::from(source.len() as u32));
    assert!(attached_packages(&after).contains(&cli));
}

#[test]
fn test_library_in_function_scoped_source_is_visible_only_in_that_function() {
    // A `library()` inside a file that's `source()`d from a function body is
    // forwarded by the builder as an `Attach` scoped to that `source()` call,
    // so its `Package` layer is visible inside the function (the lazy / EOF
    // view) but not at file scope before or after it.
    let mut db = TestDb::new();
    let dplyr = install_packages(&mut db, &["dplyr"])[0];

    let helpers = make_file(&mut db, "w/helpers.R", "library(dplyr)\n");
    let script_src = "before\nf <- function() {\n  source(\"helpers.R\")\n}\nafter\n";
    let script = make_file(&mut db, "w/script.R", script_src);

    let root = workspace_root(&db, "w");
    root.set_scripts(&mut db).to(vec![helpers, script]);
    db.workspace_roots().set_roots(&mut db).to(vec![root]);

    let at = |needle: &str| {
        let offset = TextSize::from(script_src.find(needle).unwrap() as u32);
        attached_packages(&script.imports_at(&db, offset))
    };

    assert!(!at("before").contains(&dplyr));
    assert!(at("source").contains(&dplyr));
    assert!(!at("after").contains(&dplyr));
}
