use std::collections::HashSet;

use salsa::Setter;

use crate::all_known_files;
use crate::all_used_files;
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::Db;
use crate::DbInputs;
use crate::File;
use crate::FileRevision;
use crate::Package;

#[test]
fn test_file_by_path_finds_workspace_script() {
    let mut db = TestDb::new();
    let root = workspace_root(&db, "proj");
    let file = File::new(&db, file_path("proj/a.R"), FileRevision::zero(), None, None);
    root.set_scripts(&mut db).to(vec![file]);
    db.workspace_roots().set_roots(&mut db).to(vec![root]);

    assert_eq!(db.file_by_path(&file_path("proj/a.R")), Some(file));
}

#[test]
fn test_file_by_path_finds_workspace_package_file() {
    let mut db = TestDb::new();
    let root = workspace_root(&db, "proj");
    // Construct the back-pointer + forward-edge pair: create the
    // `Package` with an empty `files` list, then the `File` with the
    // `package` set, then attach the file via `set_files`. Matches the
    // shape `oak_scan`'s placement-preserving helpers will produce.
    let pkg = Package::new(
        &db,
        file_path("proj/DESCRIPTION"),
        "mypkg".to_string(),
        FileRevision::zero(),
        FileRevision::zero(),
        None,
        None,
        vec![],
        Vec::new(),
    );
    let file = File::new(
        &db,
        file_path("proj/R/foo.R"),
        FileRevision::zero(),
        None,
        Some(pkg),
    );
    pkg.set_files(&mut db).to(vec![file]);
    root.set_packages(&mut db).to(vec![pkg]);
    db.workspace_roots().set_roots(&mut db).to(vec![root]);

    assert_eq!(db.file_by_path(&file_path("proj/R/foo.R")), Some(file));
}

#[test]
fn test_file_by_path_finds_library_package_file() {
    let mut db = TestDb::new();
    let lib = library_root(&db, "libs");
    let pkg = Package::new(
        &db,
        file_path("libs/foo/DESCRIPTION"),
        "foo".to_string(),
        FileRevision::zero(),
        FileRevision::zero(),
        None,
        None,
        vec![],
        Vec::new(),
    );
    let file = File::new(
        &db,
        file_path("libs/foo/R/a.R"),
        FileRevision::zero(),
        None,
        Some(pkg),
    );
    pkg.set_files(&mut db).to(vec![file]);
    lib.set_packages(&mut db).to(vec![pkg]);
    db.library_roots().set_roots(&mut db).to(vec![lib]);

    assert_eq!(db.file_by_path(&file_path("libs/foo/R/a.R")), Some(file));
}

#[test]
fn test_file_by_path_finds_orphan_file() {
    let mut db = TestDb::new();
    let file = File::new(
        &db,
        file_path("untitled.R"),
        FileRevision::zero(),
        None,
        None,
    );
    db.orphan_root()
        .set_files(&mut db)
        .to(HashSet::from([file]));

    assert_eq!(db.file_by_path(&file_path("untitled.R")), Some(file));
}

#[test]
fn test_file_by_path_returns_none_when_absent() {
    let db = TestDb::new();
    assert_eq!(db.file_by_path(&file_path("missing.R")), None);
}

#[test]
fn test_root_path_index_invalidates_per_root() {
    // Headline claim of the per-root scaffold: writing to one root's
    // contents doesn't invalidate another root's index. We assert it
    // by counting salsa `WillExecute` events on `root_path_index`.
    let mut db = TestDb::new();
    let root_a = workspace_root(&db, "a");
    let root_b = workspace_root(&db, "b");
    let file_a = File::new(&db, file_path("a/file.R"), FileRevision::zero(), None, None);
    let file_b = File::new(&db, file_path("b/file.R"), FileRevision::zero(), None, None);
    root_a.set_scripts(&mut db).to(vec![file_a]);
    root_b.set_scripts(&mut db).to(vec![file_b]);
    db.workspace_roots()
        .set_roots(&mut db)
        .to(vec![root_a, root_b]);

    // First lookup of `file_a` hits root A's index only (the walk
    // returns early). Second lookup of `file_b` finds nothing in A's
    // cached index, then executes B's. Total: 2.
    let _ = db.file_by_path(&file_path("a/file.R"));
    let _ = db.file_by_path(&file_path("b/file.R"));
    assert_eq!(db.executions("root_path_index"), 2);

    // Add a script to root B. B's index invalidates; A's stays cached.
    let file_b2 = File::new(
        &db,
        file_path("b/other.R"),
        FileRevision::zero(),
        None,
        None,
    );
    root_b.set_scripts(&mut db).to(vec![file_b, file_b2]);

    // Look up the file in A. A's index is still cached, no re-exec.
    let _ = db.file_by_path(&file_path("a/file.R"));
    assert_eq!(db.executions("root_path_index"), 2);

    // Look up the file in B. A still cached; B's index re-executes.
    let _ = db.file_by_path(&file_path("b/file.R"));
    assert_eq!(db.executions("root_path_index"), 3);
}

#[test]
fn test_all_used_files_excludes_unrelated_library_files() {
    let mut db = TestDb::new();

    let lib = library_root(&db, "libs");
    let used_pkg = Package::new(
        &db,
        file_path("libs/used/DESCRIPTION"),
        "used".to_string(),
        FileRevision::zero(),
        FileRevision::zero(),
        None,
        None,
        vec![],
        Vec::new(),
    );
    let used_file = File::new(
        &db,
        file_path("libs/used/R/a.R"),
        FileRevision::zero(),
        None,
        Some(used_pkg),
    );
    used_pkg.set_files(&mut db).to(vec![used_file]);

    let unused_pkg = Package::new(
        &db,
        file_path("libs/unused/DESCRIPTION"),
        "unused".to_string(),
        FileRevision::zero(),
        FileRevision::zero(),
        None,
        None,
        vec![],
        Vec::new(),
    );
    let unused_file = File::new(
        &db,
        file_path("libs/unused/R/a.R"),
        FileRevision::zero(),
        None,
        Some(unused_pkg),
    );
    unused_pkg.set_files(&mut db).to(vec![unused_file]);

    lib.set_packages(&mut db).to(vec![used_pkg, unused_pkg]);
    db.library_roots().set_roots(&mut db).to(vec![lib]);

    let root = workspace_root(&db, "proj");
    let script = File::new(
        &db,
        file_path("proj/script.R"),
        FileRevision::zero(),
        Some("library(used)\n".to_string()),
        None,
    );
    root.set_scripts(&mut db).to(vec![script]);
    db.workspace_roots().set_roots(&mut db).to(vec![root]);

    // `all_known_files` doesn't know about dependencies, so both installed
    // packages show up, referenced or not.
    assert_eq!(all_known_files(&db), &vec![script, used_file, unused_file]);

    // `all_used_files` drops `unused`: nothing in the workspace references it.
    assert_eq!(all_used_files(&db), &vec![script, used_file]);
}
