use oak_package_metadata::namespace::Import;
use oak_package_metadata::namespace::Namespace;
use salsa::Setter;
use stdext::SortedVec;

use crate::tests::test_db::file_path;
use crate::tests::test_db::workspace_root;
use crate::tests::test_db::TestDb;
use crate::DbInputs;
use crate::File;
use crate::FileRevision;
use crate::Name;
use crate::NamespaceVisibility;
use crate::Package;

/// Build a workspace package in its own root and return its files in input order.
fn setup_package(
    db: &mut TestDb,
    pkg_name: &str,
    exports: &[&str],
    files: &[(&str, &str)],
) -> (Package, Vec<File>) {
    let root = workspace_root(db, &format!("workspace/{pkg_name}"));
    let (pkg, file_entities) = new_package(db, pkg_name, exports, &[], files);
    root.set_packages(db).to(vec![pkg]);
    db.workspace_roots().set_roots(db).to(vec![root]);

    (pkg, file_entities)
}

/// Build a package without installing it in a workspace root.
fn new_package(
    db: &mut TestDb,
    pkg_name: &str,
    exports: &[&str],
    imports: &[(&str, &str)],
    files: &[(&str, &str)],
) -> (Package, Vec<File>) {
    let namespace = Namespace {
        exports: SortedVec::from_vec(exports.iter().map(|export| export.to_string()).collect()),
        imports: imports
            .iter()
            .map(|(name, package)| Import {
                name: name.to_string(),
                package: package.to_string(),
            })
            .collect(),
        ..Default::default()
    };
    let pkg = Package::new(
        db,
        file_path(&format!("workspace/{pkg_name}/DESCRIPTION")),
        pkg_name.to_string(),
        FileRevision::zero(),
        FileRevision::zero(),
        None,
        Some(namespace),
        Vec::new(),
        Vec::new(),
    );

    let file_entities: Vec<File> = files
        .iter()
        .map(|(path, contents)| {
            File::new(
                db,
                file_path(path),
                FileRevision::zero(),
                Some(contents.to_string()),
                Some(pkg),
            )
        })
        .collect();
    pkg.set_files(db).to(file_entities.clone());

    (pkg, file_entities)
}

fn name<'db>(db: &'db TestDb, text: &str) -> Name<'db> {
    Name::new(db, text)
}

#[test]
fn test_exported_visible_via_exported_lookup() {
    let mut db = TestDb::new();
    let (pkg, files) = setup_package(&mut db, "pkg", &["foo"], &[(
        "workspace/pkg/R/a.R",
        "foo <- function() 1\n",
    )]);

    let defs = pkg.resolve(&db, name(&db, "foo"), NamespaceVisibility::Exported);
    assert_eq!(defs.len(), 1);
    assert_eq!(defs[0].file(&db), files[0]);
    assert_eq!(defs[0].name(&db).text(&db).as_str(), "foo");
}

#[test]
fn test_exported_lookup_filters_unexported_name() {
    // `foo` is bound in the package but not in NAMESPACE.exports. The
    // `Exported` lookup must not surface it.
    let mut db = TestDb::new();
    let (pkg, _files) = setup_package(&mut db, "pkg", &[], &[(
        "workspace/pkg/R/a.R",
        "foo <- function() 1\n",
    )]);

    let defs = pkg.resolve(&db, name(&db, "foo"), NamespaceVisibility::Exported);
    assert!(defs.is_empty());
}

#[test]
fn test_internal_lookup_finds_unexported_name() {
    // `pkg:::foo` sees the binding even though it's not in NAMESPACE.exports.
    let mut db = TestDb::new();
    let (pkg, files) = setup_package(&mut db, "pkg", &[], &[(
        "workspace/pkg/R/a.R",
        "foo <- function() 1\n",
    )]);

    let defs = pkg.resolve(&db, name(&db, "foo"), NamespaceVisibility::Internal);
    assert_eq!(defs.len(), 1);
    assert_eq!(defs[0].file(&db), files[0]);
}

#[test]
fn test_internal_lookup_also_finds_exported_name() {
    // `:::` is a superset: exported names are also reachable via Internal.
    let mut db = TestDb::new();
    let (pkg, files) = setup_package(&mut db, "pkg", &["foo"], &[(
        "workspace/pkg/R/a.R",
        "foo <- function() 1\n",
    )]);

    let defs = pkg.resolve(&db, name(&db, "foo"), NamespaceVisibility::Internal);
    assert_eq!(defs.len(), 1);
    assert_eq!(defs[0].file(&db), files[0]);
}

#[test]
fn test_unknown_name_returns_empty() {
    let mut db = TestDb::new();
    let (pkg, _files) = setup_package(&mut db, "pkg", &["foo"], &[(
        "workspace/pkg/R/a.R",
        "foo <- function() 1\n",
    )]);

    assert!(pkg
        .resolve(&db, name(&db, "nope"), NamespaceVisibility::Exported)
        .is_empty());
    assert!(pkg
        .resolve(&db, name(&db, "nope"), NamespaceVisibility::Internal)
        .is_empty());
}

#[test]
fn test_resolves_binding_in_correct_file_for_multi_file_package() {
    // `foo` is in `a.R`, `bar` is in `b.R`. Each `pkg::name` lookup
    // lands on the right file.
    let mut db = TestDb::new();
    let (pkg, files) = setup_package(&mut db, "pkg", &["foo", "bar"], &[
        ("workspace/pkg/R/a.R", "foo <- function() 1\n"),
        ("workspace/pkg/R/b.R", "bar <- function() 2\n"),
    ]);

    let foo_defs = pkg.resolve(&db, name(&db, "foo"), NamespaceVisibility::Exported);
    assert_eq!(foo_defs.len(), 1);
    assert_eq!(foo_defs[0].file(&db), files[0]);

    let bar_defs = pkg.resolve(&db, name(&db, "bar"), NamespaceVisibility::Exported);
    assert_eq!(bar_defs.len(), 1);
    assert_eq!(bar_defs[0].file(&db), files[1]);
}

#[test]
fn test_conditional_binding_fans_out() {
    // Top-level `if (cond) foo <- 1 else foo <- 2` produces two candidate
    // Definitions for `foo`, one per branch. Multi-target exports surface both,
    // so `Package::resolve` returns both candidates from the single file.
    let mut db = TestDb::new();
    let (pkg, _files) = setup_package(&mut db, "pkg", &["foo"], &[(
        "workspace/pkg/R/a.R",
        "if (cond) foo <- 1 else foo <- 2\n",
    )]);

    let defs = pkg.resolve(&db, name(&db, "foo"), NamespaceVisibility::Exported);
    assert_eq!(defs.len(), 2);
}

#[test]
fn test_stub_and_onload_override_both_returned() {
    // The R-package idiom: a top-level stub plus a runtime override
    // installed from `.onLoad` via `<<-`. Both bindings live in the
    // package namespace, so `Package::resolve` returns both.
    let mut db = TestDb::new();
    let (pkg, files) = setup_package(&mut db, "pkg", &["foo"], &[
        ("workspace/pkg/R/foo.R", "foo <- function() stop('stub')\n"),
        (
            "workspace/pkg/R/zzz.R",
            ".onLoad <- function(libname, pkgname) {\n  foo <<- function() 'real'\n}\n",
        ),
    ]);

    let defs = pkg.resolve(&db, name(&db, "foo"), NamespaceVisibility::Exported);
    assert_eq!(defs.len(), 2);
    let target_files: Vec<File> = defs.iter().map(|d| d.file(&db)).collect();
    assert!(target_files.contains(&files[0]));
    assert!(target_files.contains(&files[1]));
}

#[test]
fn test_reexport_via_import_from_resolves_to_source() {
    // dplyr re-exports tibble's `tibble`: NAMESPACE carries `export(tibble)`
    // plus `importFrom(tibble, tibble)`, and the only R source is a bare
    // `tibble::tibble` expression (not an assignment). The binding lives in
    // tibble, so `dplyr::tibble` must follow the import and resolve there.
    let mut db = TestDb::new();
    let root = workspace_root(&db, "workspace");

    let (tibble, tibble_files) = new_package(&mut db, "tibble", &["tibble"], &[], &[(
        "workspace/tibble/R/tibble.R",
        "tibble <- function() 1\n",
    )]);
    let (dplyr, _dplyr_files) =
        new_package(&mut db, "dplyr", &["tibble"], &[("tibble", "tibble")], &[(
            "workspace/dplyr/R/reexport.R",
            "tibble::tibble\n",
        )]);

    root.set_packages(&mut db).to(vec![tibble, dplyr]);
    db.workspace_roots().set_roots(&mut db).to(vec![root]);

    let defs = dplyr.resolve(&db, name(&db, "tibble"), NamespaceVisibility::Exported);
    assert_eq!(defs.len(), 1);
    assert_eq!(defs[0].file(&db), tibble_files[0]);
    assert_eq!(defs[0].name(&db).text(&db).as_str(), "tibble");
}

#[test]
fn test_self_reexport_does_not_cycle() {
    let mut db = TestDb::new();
    let root = workspace_root(&db, "workspace");

    let (pkg, _files) = new_package(&mut db, "pkg", &["foo"], &[("foo", "pkg")], &[(
        "workspace/pkg/R/a.R",
        "bar <- function() 1\n",
    )]);

    root.set_packages(&mut db).to(vec![pkg]);
    db.workspace_roots().set_roots(&mut db).to(vec![root]);

    assert!(pkg
        .resolve(&db, name(&db, "foo"), NamespaceVisibility::Exported)
        .is_empty());
    assert!(pkg
        .resolve(&db, name(&db, "foo"), NamespaceVisibility::Internal)
        .is_empty());
}

#[test]
fn test_mutual_reexport_does_not_cycle() {
    let mut db = TestDb::new();
    let root = workspace_root(&db, "workspace");

    let (a, _a_files) = new_package(&mut db, "a", &["foo"], &[("foo", "b")], &[(
        "workspace/a/R/a.R",
        "b::foo\n",
    )]);
    let (b, _b_files) = new_package(&mut db, "b", &["foo"], &[("foo", "a")], &[(
        "workspace/b/R/b.R",
        "a::foo\n",
    )]);

    root.set_packages(&mut db).to(vec![a, b]);
    db.workspace_roots().set_roots(&mut db).to(vec![root]);

    assert!(a
        .resolve(&db, name(&db, "foo"), NamespaceVisibility::Exported)
        .is_empty());
    assert!(b
        .resolve(&db, name(&db, "foo"), NamespaceVisibility::Exported)
        .is_empty());
}

#[test]
fn test_same_name_defined_in_multiple_files_returns_each() {
    // Two files in the package both define `foo`. `Package::resolve`
    // surfaces both candidates. (R's runtime would have one wins via
    // collation order, but goto-def should offer all candidates.)
    let mut db = TestDb::new();
    let (pkg, files) = setup_package(&mut db, "pkg", &["foo"], &[
        ("workspace/pkg/R/a.R", "foo <- function() 1\n"),
        ("workspace/pkg/R/b.R", "foo <- function() 2\n"),
    ]);

    let defs = pkg.resolve(&db, name(&db, "foo"), NamespaceVisibility::Exported);
    assert_eq!(defs.len(), 2);
    let target_files: Vec<File> = defs.iter().map(|d| d.file(&db)).collect();
    assert!(target_files.contains(&files[0]));
    assert!(target_files.contains(&files[1]));
}

#[test]
fn test_base_exports_every_top_level_binding() {
    // `base` doesn't have a `NAMESPACE`, so there are no namespace exports for it.
    // Instead, `NamespaceVisibility::Exported` lookups on `base` skip the export check and
    // fall straight through to looking for a top-level binding anywhere in the `base`
    // source files.
    //
    // Note that this won't find primitives! These have no top-level binding.
    let mut db = TestDb::new();
    let (pkg, files) = setup_package(&mut db, "base", &[], &[(
        "workspace/base/R/a.R",
        "foo <- function() 1\n",
    )]);

    let defs = pkg.resolve(&db, name(&db, "foo"), NamespaceVisibility::Exported);
    assert_eq!(defs.len(), 1);
    assert_eq!(defs[0].file(&db), files[0]);
    assert_eq!(defs[0].name(&db).text(&db).as_str(), "foo");
}
