tests/testthat/test-build_lib.R

tiny_build_lib <- function() {
  wavenumber <- seq(100, 6100, by = 100)
  base_a <- dnorm(seq(-3, 3, length.out = length(wavenumber)))
  base_b <- rev(cumsum(seq_along(wavenumber)) / sum(seq_along(wavenumber)))
  spectra <- sapply(seq_len(8), function(i) {
    if (i <= 4) base_a + i / 20 else base_b + i / 20
  })
  colnames(spectra) <- paste0("s", seq_len(ncol(spectra)))
  as_OpenSpecy(
    wavenumber,
    spectra = spectra,
    metadata = data.table::data.table(
      sample_name = colnames(spectra),
      source = c("A", "B", "C", "C", "A", "B", "C", "C"),
      label = c("nylon 6", "polyamides", "plastic", "missing",
                "pet", "polyesters", "plastic", "missing"),
      material_class = rep(c("class_a", "class_b"), each = 4),
      spectrum_type = rep("ftir", 8),
      intensity_units = rep("absorbance", 8)
    ),
    attributes = list(intensity_unit = "absorbance")
  )
}

reference_workflow_data_path <- function(file) {
  roots <- c(
    testthat::test_path("..", ".."),
    Sys.getenv("GITHUB_WORKSPACE", unset = "")
  )
  roots <- unique(roots[nzchar(roots)])
  candidates <- file.path(roots, "workflows", "data", file)
  existing <- candidates[file.exists(candidates)]
  testthat::skip_if(
    length(existing) == 0L,
    "repository-only reference workflow tables are not installed"
  )
  existing[[1L]]
}

test_that("make_lib_lookup_template() returns or writes deduplicated templates", {
  lib <- tiny_build_lib()

  template <- make_lib_lookup_template(lib, columns = "source",
                                       add = "LibraryType")
  expect_s3_class(template, "data.table")
  expect_equal(sort(template$source), c("A", "B", "C"))
  expect_true("LibraryType" %in% names(template))
  expect_true(all(is.na(template$LibraryType)))

  tmp <- tempfile(fileext = ".csv")
  invisible(make_lib_lookup_template(lib, columns = "source",
                                     add = "LibraryType", path = tmp))
  expect_true(file.exists(tmp))
})

test_that("join_lib_metadata() reports incomplete and duplicate joins", {
  lib <- tiny_build_lib()
  lookup <- data.table::data.table(source = c("A", "B"),
                                   LibraryType = c("type_a", "type_b"))

  expect_warning(
    joined <- join_lib_metadata(lib, lookup, by = "source"),
    "unmatched_metadata_key"
  )
  expect_true(check_OpenSpecy(joined))
  expect_equal(nrow(joined$metadata), ncol(joined$spectra))
  expect_true("source" %in% names(joined$metadata))
  expect_false(any(c("source.x", "source.y") %in% names(joined$metadata)))
  expect_true("LibraryType" %in% names(joined$metadata))
  expect_error(
    join_lib_metadata(lib, lookup, by = "source", require_complete = TRUE),
    "unmatched_metadata_key"
  )

  dup_lookup <- data.table::data.table(source = c("A", "A"),
                                       LibraryType = c("x", "y"))
  expect_error(join_lib_metadata(lib, dup_lookup, by = "source"),
               "unique")
})

test_that("join_material_hierarchy() matches user-specified levels", {
  lib <- tiny_build_lib()
  hierarchy <- data.table::data.table(
    material = c("nylon 6", "pet"),
    material_class = c("polyamides", "polyesters"),
    material_type = c("plastic", "plastic")
  )

  expect_warning(
    joined <- join_material_hierarchy(
      lib,
      hierarchy = hierarchy,
      key_col = "label",
      levels = c("material", "material_class", "material_type"),
      output_names = c(material = "joined_material",
                       material_class = "joined_class",
                       material_type = "joined_type")
    ),
    "unmatched_hierarchy_key"
  )

  expect_true(check_OpenSpecy(joined))
  expect_equal(joined$metadata$joined_material[1], "nylon 6")
  expect_equal(joined$metadata$joined_class[2], "polyamides")
  expect_equal(joined$metadata$joined_type[3], "plastic")
  expect_true(is.na(joined$metadata$joined_type[4]))
})

test_that("dedupe_spec() keeps identifiers aligned", {
  lib <- tiny_build_lib()
  lib$spectra[, 2] <- lib$spectra[, 1]

  deduped <- dedupe_spec(lib)
  expect_true(check_OpenSpecy(deduped))
  expect_equal(ncol(deduped$spectra), 7)
  expect_identical(colnames(deduped$spectra),
                   deduped$metadata$sample_name)
})

test_that("build_lib() uses legacy source-stage hashes for sample_name", {
  wavenumber <- seq(50, 5000, by = 5)
  spectra <- cbind(
    sin(wavenumber / 200) + 2,
    cos(wavenumber / 250) + 2
  )
  colnames(spectra) <- c("raw_a", "raw_b")
  lib <- as_OpenSpecy(
    wavenumber,
    spectra = spectra,
    metadata = data.table::data.table(sample_name = colnames(spectra)),
    attributes = list(intensity_unit = "absorbance")
  )

  legacy_hash <- function(x, range = NULL, short_value = NULL) {
    x <- manage_na(x, type = "remove")
    spec <- conform_spec(x, range = range, res = 8)
    if (!is.null(short_value) && nrow(spec$spectra) < 3) {
      return(rep(short_value, ncol(x$spectra)))
    }
    spec <- smooth_intens(spec)
    vapply(seq_len(ncol(spec$spectra)), function(i) {
      digest::digest(
        list(as.integer(spec$wavenumber),
             as.integer(spec$spectra[, i] * 100)),
        algo = "md5"
      )
    }, FUN.VALUE = character(1))
  }

  expected <- legacy_hash(lib)
  expected_old <- legacy_hash(lib, range = c(100, 4000),
                              short_value = "new format")
  built <- build_lib(
    lib,
    recipes = list(raw = list()),
    dedupe = TRUE,
    convert_intensity = FALSE,
    signal_noise = FALSE,
    progress = FALSE
  )$raw

  expect_equal(built$metadata$sample_name, expected)
  expect_equal(built$metadata$sample_name_old, expected_old)
  expect_equal(colnames(built$spectra), expected)

  excluded <- build_lib(
    lib,
    recipes = list(raw = list()),
    exclude_ids = expected_old[1],
    dedupe = TRUE,
    convert_intensity = FALSE,
    signal_noise = FALSE,
    progress = FALSE
  )$raw
  expect_equal(excluded$metadata$sample_name, expected[2])
})

test_that("reduce_lib() returns medoid ids or reduced OpenSpecy objects", {
  lib <- tiny_build_lib()

  ids <- reduce_lib(lib, group_cols = "material_class", k = 2, min_n = 2,
                    return = "ids")
  expect_equal(length(ids), 4)

  reduced <- reduce_lib(lib, group_cols = "material_class", k = 2, min_n = 2)
  expect_true(check_OpenSpecy(reduced))
  expect_equal(ncol(reduced$spectra), 4)
})

test_that("reduce_lib() uses cluster PAM medoids and reports useful progress", {
  lib <- tiny_build_lib()
  ids <- .lib_ids(lib, "sample_name")
  reduction_obj <- lib
  reduction_obj$spectra <- make_rel(
    mean_replace(lib$spectra, na.rm = TRUE), na.rm = TRUE
  )
  reduction_obj$spectra[!is.finite(reduction_obj$spectra)] <- 0
  groups <- do.call(paste, c(lib$metadata[, "material_class", with = FALSE],
                             sep = "_"))

  expected <- unlist(lapply(split(seq_along(groups), groups), function(idx) {
    if (length(idx) <= 2L) return(ids[idx])
    x <- filter_spec(reduction_obj, idx)
    cors <- cor_spec(x, x, compute = "optimized")
    cors[is.na(cors)] <- 0
    cors <- pmax(pmin(cors, 1), -1)
    diag(cors) <- 1
    distance <- stats::as.dist(1 - cors)
    set.seed(OpenSpecy:::.lib_pam_seed(ids[idx]))
    ids[idx][cluster::pam(
      distance, k = 2, diss = TRUE, variant = "faster"
    )$id.med]
  }), use.names = FALSE)

  messages <- capture.output(
    actual <- reduce_lib(
      lib, group_cols = "material_class", k = 2, min_n = 2,
      return = "ids", progress = TRUE
    ),
    type = "message"
  )
  expect_equal(actual, expected)
  messages <- paste(messages, collapse = "\n")
  expect_match(messages, "spectra in .* groups")
  expect_match(messages, "PAM group 1/2 starting")
  expect_match(messages, "correlation complete")
  expect_match(messages, "PAM complete")
  expect_match(messages, "variant=faster")
  expect_match(messages, "kept=4/8")

  expect_silent(
    reduce_lib(lib, group_cols = "material_class", k = 2, min_n = 2,
               return = "ids", progress = FALSE)
  )
  expect_error(reduce_lib(lib, progress = NA), "'progress'")
})

test_that("oversized PAM groups use deterministic sampled correlation medoids", {
  set.seed(42)
  wn <- seq(800, 3200, length.out = 60)
  spectra <- matrix(runif(length(wn) * 120L), nrow = length(wn))
  colnames(spectra) <- paste0("large_", seq_len(ncol(spectra)))
  lib <- as_OpenSpecy(
    wn, spectra,
    metadata = data.table::data.table(sample_name = colnames(spectra))
  )

  messages <- capture.output(
    first <- OpenSpecy:::.pam_large_group_ids(
      lib, id_col = "sample_name", k = 5L, progress = TRUE,
      group_label = "test", samples = 3L, sample_size = 60L
    ),
    type = "message"
  )
  second <- OpenSpecy:::.pam_large_group_ids(
    lib, id_col = "sample_name", k = 5L, progress = FALSE,
    group_label = "test", samples = 3L, sample_size = 60L
  )

  expect_identical(first, second)
  expect_length(first, 5L)
  expect_true(all(first %in% colnames(spectra)))
  expect_match(paste(messages, collapse = "\n"), "sampled PAM complete")
})

test_that("build_model_lib() returns the model library artifact structure", {
  skip_if_not_installed("glmnet")
  lib <- tiny_build_lib()

  model <- suppressWarnings(
    build_model_lib(lib, type_col = NULL, min_n = 2, nlambda = 3)
  )
  expect_named(model, c("model", "model_type", "lambda_selected",
                        "lambda_path_complete", "selected_lambda_converged",
                        "convergence_error", "training_groups", "selection_metric",
                        "lambda_metrics", "dimension_conversion", "tests",
                        "coefficients", "class_names", "class_num",
                        "observation_count", "fill", "support",
                        "class_support", "fill_method", "fill_replaced",
                        "variable_num",
                        "all_variables", "variables_in"))
  expect_true(is_OpenSpecy(model$fill))
  expect_identical(model$model_type, "logistic_regression")
  expect_identical(model$selection_metric, "macro_class_accuracy")
  expect_identical(model$fill_method, "wavenumber_mean")
  expect_true(all(c("lambda", "macro_class_accuracy", "overall_accuracy",
                    "selected", "selection_scope", "selection_rule") %in%
                    names(model$lambda_metrics)))
  expect_true(all(c("factor_num", "name") %in%
                    names(model$dimension_conversion)))
  expect_true(all(c("spectrum_id", "technique", "expected_class",
                    "predicted_class", "correct", "score", "split",
                    "provenance") %in% names(model$tests)))
  expect_null(model$model$call)

  before <- suppressWarnings(match_spec(lib, library = model))
  full_bytes <- length(serialize(model$model, NULL))
  slim <- OpenSpecy:::.lib_slim_model(model)
  after <- suppressWarnings(match_spec(lib, library = slim))
  expect_length(slim$model$lambda, 1L)
  expect_lt(length(serialize(slim$model, NULL)), full_bytes)
  expect_equal(after, before, tolerance = 1e-12)
})

test_that("release sanitizers retain runtime state and remove assessments", {
  lib <- tiny_build_lib()
  attr(lib, "derivative_order") <- "1"
  attr(lib, "transformations") <- list(list(method = "derivative"))
  attr(lib, "quality_control_report") <- data.table::data.table(value = 1)
  attr(lib, "prune_report") <- list(summary = data.table::data.table(value = 1))
  attr(lib$metadata, "join_report") <- data.table::data.table(value = 1)

  slim <- OpenSpecy:::.lib_slim_reference_object(lib)
  expect_true(check_OpenSpecy(slim))
  expect_identical(attr(slim, "derivative_order"), "1")
  expect_equal(attr(slim, "transformations"), list(list(method = "derivative")))
  expect_null(attr(slim, "quality_control_report", exact = TRUE))
  expect_null(attr(slim, "prune_report", exact = TRUE))
  expect_null(attr(slim$metadata, "join_report", exact = TRUE))
  expect_identical(colnames(slim$spectra), slim$metadata$sample_name)

  model <- list(
    model = structure(list(), class = "mock_model"),
    model_type = "logistic_regression", lambda_selected = 0.1,
    dimension_conversion = data.table::data.table(factor_num = 1L, name = "a"),
    coefficients = data.table::data.table(), class_names = "a", class_num = 1L,
    observation_count = 8L, fill = lib, fill_method = "wavenumber_mean",
    variable_num = nrow(lib$spectra), all_variables = lib$wavenumber,
    variables_in = lib$wavenumber,
    tests = data.table::data.table(correct = TRUE),
    lambda_metrics = data.table::data.table(lambda = 0.1),
    support = data.table::data.table(spectrum_id = "s1")
  )
  attr(model, "training_warnings") <- "diagnostic warning"
  slim_model <- OpenSpecy:::.lib_slim_model(model)
  expect_true(all(names(slim_model) %in%
                    OpenSpecy:::.lib_runtime_model_fields()))
  expect_false(any(c("tests", "lambda_metrics", "support") %in%
                     names(slim_model)))
  expect_null(attr(slim_model, "training_warnings", exact = TRUE))
  expect_true(check_OpenSpecy(slim_model$fill))
  expect_null(attr(slim_model$fill, "quality_control_report", exact = TRUE))
  expect_true(OpenSpecy:::.lib_selected_lambda_converged(list(
    model = structure(list(jerr = 0L), class = "glmnet")
  )))
  expect_false(OpenSpecy:::.lib_selected_lambda_converged(list(
    model = structure(list(jerr = -3L), class = "glmnet")
  )))
  expect_false(OpenSpecy:::.lib_selected_lambda_converged(list(
    model = structure(list(jerr = 0L), class = "glmnet"),
    selected_lambda_converged = FALSE
  )))
})

test_that("official builders can opt into host workers without changing defaults", {
  previous <- options("OpenSpecy.build_workers")
  on.exit(options(previous), add = TRUE)

  options(OpenSpecy.build_workers = NULL)
  expect_identical(OpenSpecy:::.lib_build_workers(), 1L)
  options(OpenSpecy.build_workers = 8L)
  expect_identical(OpenSpecy:::.lib_build_workers(), 8L)
  options(OpenSpecy.build_workers = -1L)
  expect_identical(OpenSpecy:::.lib_build_workers(), 1L)
})

test_that("build_model_lib() trains and deploys a balanced probability random forest", {
  skip_if_not_installed("ranger")
  lib <- tiny_build_lib()
  lib$spectra[1:3, 1] <- NA_real_
  lib$metadata$material_class <- c(rep("class_a", 6), rep("class_b", 2))

  model <- build_model_lib(
    lib, type_col = NULL, min_n = 2, method = "random_forest",
    num.trees = 30L, min.node.size = 1L, mtry = 4L
  )
  expect_identical(model$model_type, "random_forest")
  expect_s3_class(model$model, "ranger")
  expect_true(model$fill_replaced >= 3L)
  expect_true(all(c("accuracy", "macro_class_accuracy", "mean_score",
                    "brier_score") %in% names(model$oob_metrics)))
  expect_true(all(c("wavenumber", "importance") %in%
                    names(model$feature_importance)))
  expect_equal(model$class_weights$weight, c(2 / 3, 2))
  expect_true(all(model$class_weights$application == "case_sampling"))
  expect_identical(
    model$training_parameters$balance_method,
    "inverse_frequency_case_sampling"
  )
  expect_identical(model$training_parameters$num_threads, 1L)
  expect_equal(nrow(model$tests), ncol(lib$spectra))

  prediction <- suppressWarnings(match_spec(lib, library = model))
  expect_equal(nrow(prediction), ncol(lib$spectra))
  expect_true(all(prediction$name %in% c("class_a", "class_b")))
})

test_that("official model building records fit warnings and progress", {
  lib <- tiny_build_lib()
  messages <- character()
  local_mocked_bindings(
    train_spec_model = function(...) {
      warning("mock convergence warning", call. = FALSE)
      list(
        observation_count = 4L, class_num = 2L,
        lambda_selected = 0.1
      )
    },
    .package = "OpenSpecy"
  )

  result <- OpenSpecy:::.lib_build_models(
    libraries = list(),
    medoids = list(derivative = list(nir = lib)),
    report = function(message) messages <<- c(messages, message)
  )
  expect_identical(
    result$warnings,
    data.table::data.table(
      algorithm = "logistic_regression", artifact = "derivative", model = "nir",
      warning = "mock convergence warning"
    )
  )
  expect_identical(
    attr(result$models$logistic_regression$derivative$nir,
         "training_warnings"),
    "mock convergence warning"
  )
  expect_true(any(grepl("model warning", messages, fixed = TRUE)))
  expect_true(any(grepl("model complete", messages, fixed = TRUE)))
})

test_that("official random forests train on full libraries rather than medoids", {
  skip_if_not_installed("ranger")
  base <- tiny_build_lib()
  full <- base
  full$spectra <- do.call(cbind, lapply(1:3, function(copy) {
    values <- base$spectra + copy / 1000
    colnames(values) <- paste0(colnames(values), "_", copy)
    values
  }))
  full$metadata <- data.table::rbindlist(lapply(1:3, function(copy) {
    metadata <- data.table::copy(base$metadata)
    metadata$sample_name <- paste0(metadata$sample_name, "_", copy)
    metadata
  }))
  full <- as_OpenSpecy(full)

  result <- OpenSpecy:::.lib_build_models(
    libraries = list(raw = list(ftir = full)), medoids = list(),
    report = function(...) NULL,
    methods = "random_forest",
    random_forest_args = list(num.trees = 20L, min.node.size = 1L, mtry = 4L)
  )
  model <- result$models$random_forest$raw$ftir
  expect_identical(model$model_type, "random_forest")
  expect_equal(model$observation_count, ncol(full$spectra))
  expect_equal(model$training_parameters$num_trees, 20L)
})

test_that("typed random forests do not repeat FTIR and Raman in a combined fit", {
  lib <- tiny_build_lib()
  local_mocked_bindings(
    train_spec_model = function(x, ...) {
      list(
        model_type = "random_forest",
        observation_count = ncol(x$spectra), class_num = 2L,
        fill_replaced = 0L,
        oob_metrics = data.table::data.table(macro_class_accuracy = 0.5),
        training_parameters = data.table::data.table(
          num_trees = 20L, mtry = 2L
        )
      )
    },
    .package = "OpenSpecy"
  )

  result <- OpenSpecy:::.lib_build_models(
    libraries = list(raw = list(ftir = lib, raman = lib)),
    medoids = list(), report = function(...) NULL,
    methods = "random_forest"
  )

  expect_named(result$models$random_forest$raw, c("ftir", "raman"))
})

test_that("build_lib() applies named recipes to merged sources", {
  lib <- tiny_build_lib()
  built <- build_lib(
    list(lib),
    recipes = list(raw = list(),
                   relative = function(x) make_rel(x, na.rm = TRUE)),
    dedupe = FALSE,
    signal_noise = FALSE
  )

  expect_named(built, c("raw", "relative"))
  expect_true(check_OpenSpecy(built$raw))
  expect_true(check_OpenSpecy(built$relative))
})

test_that("build_lib() requires explicit source inputs", {
  expect_error(
    build_lib(),
    "'x' must specify the source library file path"
  )
})

test_that("build_lib() converts metadata intensity units before recipes", {
  lib <- tiny_build_lib()
  lib$spectra <- matrix(
    rep(c(50, 25, 0.5, 0.25, 2), each = nrow(lib$spectra)),
    nrow = nrow(lib$spectra),
    dimnames = list(NULL, paste0("u", 1:5))
  )
  lib$spectra[1, 2] <- NA_real_
  lib$metadata <- data.table::data.table(
    sample_name = colnames(lib$spectra),
    intensity_units = c(
      "Reflectance (%)", "transmittance", "absorbance", "mystery", NA
    )
  )
  attr(lib, "intensity_unit") <- NULL
  original <- lib$spectra

  expect_warning(
    built <- build_lib(
      list(lib),
      recipes = list(raw = list()),
      dedupe = FALSE,
      signal_noise = FALSE
    )$raw,
    "skipped 2 spectrum/s.*<missing> \\(1\\).*mystery \\(1\\)|skipped 2 spectrum/s.*mystery \\(1\\).*<missing> \\(1\\)"
  )

  expect_equal(
    built$spectra[, 1],
    adj_intens(original[, 1], type = "reflectance", make_rel = FALSE)
  )
  expect_equal(
    built$spectra[, 2],
    adj_intens(
      original[, 2], type = "transmittance", make_rel = FALSE,
      na.rm = TRUE
    )
  )
  expect_equal(built$spectra[, 3:5], original[, 3:5])
  expect_equal(
    built$metadata$intensity_units,
    c("absorbance", "absorbance", "absorbance", "mystery", NA)
  )
  expect_null(attr(built, "intensity_unit"))
  expect_true(check_OpenSpecy(built))
})

test_that("build_lib() treats intensity_unit attribute as primary truth", {
  lib <- tiny_build_lib()
  lib$spectra[,] <- 50
  lib$metadata$intensity_units <- "transmittance"
  attr(lib, "intensity_unit") <- "reflectance"

  built <- build_lib(
    list(lib),
    recipes = list(raw = list()),
    dedupe = FALSE,
    signal_noise = FALSE
  )$raw

  expect_equal(
    built$spectra,
    adj_intens(lib$spectra, type = "reflectance", make_rel = FALSE)
  )
  expect_equal(built$metadata$intensity_units, rep("absorbance", 8))
  expect_equal(attr(built, "intensity_unit"), "absorbance")

  attr(lib, "intensity_unit") <- "absorbance"
  unchanged <- build_lib(
    list(lib),
    recipes = list(raw = list()),
    dedupe = FALSE,
    signal_noise = FALSE
  )$raw
  expect_equal(unchanged$spectra, lib$spectra)
  expect_equal(unchanged$metadata$intensity_units, rep("absorbance", 8))
})

test_that("build_lib() can preserve declared intensity units", {
  lib <- tiny_build_lib()
  lib$spectra[,] <- 50
  lib$metadata$intensity_units <- "reflectance"
  attr(lib, "intensity_unit") <- "reflectance"

  built <- build_lib(
    list(lib),
    recipes = list(raw = list()),
    dedupe = FALSE,
    convert_intensity = FALSE,
    signal_noise = FALSE
  )$raw

  expect_equal(built$spectra, lib$spectra)
  expect_equal(built$metadata$intensity_units, rep("reflectance", 8))
  expect_equal(attr(built, "intensity_unit"), "reflectance")

  lib$metadata$intensity_units <- "transmittance"
  preserved <- build_lib(
    lib,
    recipes = list(raw = list()),
    dedupe = FALSE,
    convert_intensity = FALSE,
    signal_noise = FALSE,
    progress = FALSE
  )$raw
  expect_equal(preserved$spectra, lib$spectra)
  expect_equal(preserved$metadata$intensity_units, rep("transmittance", 8))
  expect_equal(attr(preserved, "intensity_unit"), "reflectance")
})

test_that("build_lib() accepts and restricts one OpenSpecy object", {
  lib <- tiny_build_lib()
  expect_message(
    built <- build_lib(
      lib,
      recipes = list(raw = list()),
      restrict_range_args = list(
        min = c(100, 2500),
        max = c(2000, 4000)
      ),
      dedupe = FALSE,
      signal_noise = FALSE
    )$raw,
    "using one in-memory OpenSpecy source"
  )

  keep <- lib$wavenumber <= 2000 |
    (lib$wavenumber >= 2500 & lib$wavenumber <= 4000)
  expect_equal(built$wavenumber, lib$wavenumber[keep])
  expect_equal(built$spectra, lib$spectra[keep, , drop = FALSE])
  expect_silent(build_lib(
    lib,
    recipes = list(raw = list()),
    dedupe = FALSE,
    signal_noise = FALSE,
    progress = FALSE
  ))
  expect_error(
    build_lib(list(lib), restrict_range_args = list(c(100, 2000))),
    "named list"
  )
})

test_that("build_lib() reads one or many OpenSpecy objects from each RDS", {
  left <- filter_spec(tiny_build_lib(), 1:2)
  right <- filter_spec(tiny_build_lib(), 3:4)
  single_file <- tempfile(fileext = ".rds")
  list_file <- tempfile(fileext = ".RDS")
  invalid_file <- tempfile(fileext = ".rds")
  saveRDS(left, single_file)
  saveRDS(list(left, right), list_file)
  saveRDS(list(left, "not OpenSpecy"), invalid_file)

  single <- build_lib(
    single_file,
    recipes = list(raw = list()),
    dedupe = FALSE,
    signal_noise = FALSE,
    progress = FALSE
  )$raw
  combined <- build_lib(
    list_file,
    recipes = list(raw = list()),
    range = NULL,
    dedupe = FALSE,
    signal_noise = FALSE,
    progress = FALSE
  )$raw

  expect_equal(single$spectra, left$spectra)
  expect_equal(ncol(combined$spectra), 4)
  expect_equal(combined$metadata$sample_name, paste0("s", 1:4))
  expect_error(
    build_lib(invalid_file, progress = FALSE),
    "File path 1 must contain one OpenSpecy object or a nonempty list"
  )
})

test_that("build_lib() bulk-prepares legacy same-axis source lists", {
  lib <- tiny_build_lib()
  sources <- split_spec(list(lib))
  sources <- lapply(sources, function(x) {
    x$spectra <- data.table::as.data.table(x$spectra)
    x
  })

  expect_silent(
    built <- build_lib(
      sources,
      recipes = list(raw = list()),
      range = NULL,
      dedupe = FALSE,
      signal_noise = FALSE,
      progress = FALSE
    )$raw
  )

  expect_equal(built$wavenumber, lib$wavenumber)
  expect_equal(built$spectra, lib$spectra)
  expect_equal(built$metadata$sample_name, lib$metadata$sample_name)
})

test_that("build_lib() converts each source before merging", {
  left <- filter_spec(tiny_build_lib(), 1:2)
  right <- filter_spec(tiny_build_lib(), 3:4)
  left$spectra[,] <- 50
  right$spectra[,] <- 0.5
  attr(left, "intensity_unit") <- "reflectance"
  attr(right, "intensity_unit") <- NULL
  right$metadata$intensity_units <- "transmittance"

  built <- build_lib(
    list(left, right),
    recipes = list(raw = list()),
    range = NULL,
    dedupe = FALSE,
    signal_noise = FALSE
  )$raw

  expected <- cbind(
    adj_intens(left$spectra, type = "reflectance", make_rel = FALSE),
    adj_intens(right$spectra, type = "transmittance", make_rel = FALSE)
  )
  colnames(expected) <- colnames(built$spectra)
  expect_equal(built$spectra, expected)
  expect_equal(attr(built, "intensity_unit"), "absorbance")
  expect_equal(built$metadata$intensity_units, rep("absorbance", 4))
})

test_that("metadata name helpers support smart and extensible matching", {
  expect_equal(
    lib_clean_name(c(" User Name ", "Laser (%)", "Method...3")),
    c("user_name", "laser_perc", "method_3")
  )

  name_lookup <- lib_metadata_name_lookup(
    campaign_code = "campaign id",
    project_code = character(),
    review_note = character(),
    regex = list(instrument_mode = "^method_[0-9]+$")
  )
  expect_false(any(c("username", "samplename", "librarytype") %in%
                     name_lookup$source_name, na.rm = TRUE))

  metadata <- data.table::data.table(
    UserName = c("alias_a", NA),
    user_name = c(NA, "canonical_b"),
    ProjectCodes = c("p1", "p2"),
    `Review Notes` = c("check", "keep"),
    Campaign.ID = c("campaign_a", "campaign_b"),
    Method.42 = c("ftir", "raman")
  )
  cleaned <- lib_clean_metadata(metadata, name_lookup)

  expect_equal(cleaned$user_name, c("alias_a", "canonical_b"))
  expect_equal(cleaned$project_code, c("p1", "p2"))
  expect_equal(cleaned$campaign_code, c("campaign_a", "campaign_b"))
  expect_equal(cleaned$review_note, c("check", "keep"))
  expect_equal(cleaned$instrument_mode, c("ftir", "raman"))
  expect_false("campaign_id" %in% names(cleaned))

  cleaned_values <- lib_clean_metadata(
    data.table::data.table(
      Organization = c(" Monterey Bay Aquarium Research Institute ", "NULL"),
      SpectrumType = c("Raman", " not available "),
      numeric_value = c(1, 2)
    ),
    clean_values = TRUE
  )
  expect_equal(
    cleaned_values$organization,
    c("monterey bay aquarium research institute", NA)
  )
  expect_equal(cleaned_values$spectrum_type, c("raman", NA))
  expect_equal(cleaned_values$numeric_value, c(1, 2))

  strict_lookup <- lib_metadata_name_lookup(
    project_code = character(),
    defaults = FALSE,
    match_without_underscores = FALSE,
    match_singular_plural = FALSE
  )
  strict <- lib_clean_metadata(
    data.table::data.table(ProjectCodes = "p1"),
    strict_lookup
  )
  expect_named(strict, "projectcodes")
})

test_that("metadata harmonization coalesces reviewed aliases only", {
  metadata <- data.table::data.table(
    spectrum_identity = c("canonical", NA),
    interpretation = c("ignored", "interpreted"),
    form_factor = c("film", NA),
    shape = c(NA, "fiber"),
    datatype = c("absorbance", "raman shift"),
    xunits = c("cm-1", NA),
    x_unit = c(NA, "1/cm"),
    spectrumid = c("a", "b"),
    locationdescription = c("left", "right"),
    name = c("source name", "source name 2"),
    names = c("other meaning", "other meaning 2"),
    file = c("raw path", "raw path 2"),
    file_name = c("display", "display 2"),
    sample = c("source sample", "source sample 2"),
    sample_name = c("stable-a", "stable-b")
  )

  cleaned <- lib_clean_metadata(metadata)

  expect_equal(cleaned$spectrum_identity, c("canonical", "interpreted"))
  expect_equal(cleaned$material_form, c("film", "fiber"))
  expect_equal(cleaned$data_type, c("absorbance", "raman shift"))
  expect_equal(cleaned$wavenumber_units, c("cm-1", "1/cm"))
  expect_equal(cleaned$spectrum_id, c("a", "b"))
  expect_equal(cleaned$location_description, c("left", "right"))
  expect_true(all(c("name", "names", "file", "file_name", "sample",
                    "sample_name") %in% names(cleaned)))
})

test_that("metadata regex lookup reports overlapping patterns", {
  name_lookup <- lib_metadata_name_lookup(
    defaults = FALSE,
    regex = list(
      campaign = "^campaign",
      identifier = "_id$"
    )
  )

  expect_error(
    lib_clean_metadata(
      data.table::data.table(Campaign.ID = "campaign_a"),
      name_lookup
    ),
    "Multiple metadata name regular expressions.*Campaign.ID"
  )
})

test_that("build_lib() cleans and coalesces metadata column names", {
  lib <- tiny_build_lib()
  lib$metadata[["UserName"]] <- c("alias_a", "alias_b", rep(NA, 6))
  lib$metadata[["user name"]] <- c(NA, "canonical_b", rep(NA, 6))
  lib$metadata[["NumberofAccumulations"]] <- c(10L, rep(NA_integer_, 7))
  lib$metadata[["Number of sample scans"]] <- c(20L, 30L,
                                                rep(NA_integer_, 6))
  lib$metadata[["CAS REGISTRY NO"]] <- rep("25038-54-4", 8)
  lib$metadata[["Laser (%)"]] <- rep(75, 8)

  name_lookup <- lib_metadata_name_lookup(project_code = "Campaign ID")
  lib$metadata[["Campaign.ID"]] <- rep("campaign_a", 8)

  built <- build_lib(
    list(lib),
    recipes = list(raw = list()),
    metadata_name_lookup = name_lookup,
    dedupe = FALSE,
    signal_noise = FALSE
  )$raw

  expect_true(all(grepl("^[a-z0-9]+(?:_[a-z0-9]+)*$",
                        names(built$metadata))))
  expect_equal(built$metadata$user_name[1:2],
               c("alias_a", "canonical_b"))
  expect_equal(built$metadata$number_of_accumulations[1:2], c(10L, 30L))
  expect_equal(built$metadata$cas_number, rep("25038-54-4", 8))
  expect_equal(built$metadata$laser_perc, rep(75, 8))
  expect_equal(built$metadata$project_code, rep("campaign_a", 8))
  expect_false(any(c("username", "numberofaccumulations",
                     "number_of_sample_scans") %in% names(built$metadata)))
})

test_that("source metadata invalid bytes are normalized before merging", {
  invalid <- rawToChar(as.raw(c(0x66, 0x80, 0x6f)))
  Encoding(invalid) <- "unknown"
  expect_false(validUTF8(invalid))
  normalized <- OpenSpecy:::.lib_normalize_metadata_encoding(
    data.table::data.table(value = invalid)
  )
  expect_true(validUTF8(normalized$value))
  expect_equal(normalized$value, "fo")
})

test_that("build_lib() runs default joins, processing, SNR, and assessment", {
  lib <- tiny_build_lib()
  lib$spectra[1, 1] <- -1
  source_lookup <- data.table::data.table(
    Source = c("A", "B", "C"),
    Material = c("mat_a", "mat_b", "mat_c")
  )
  hierarchy <- data.table::data.table(
    Material = c("mat_a", "mat_b", "mat_c"),
    `Material Class` = c("class_a", "class_b", "class_c"),
    `Material Type` = rep("material", 3)
  )

  built <- suppressWarnings(build_lib(
    list(lib),
    metadata_lookups = source_lookup,
    material_hierarchy = hierarchy,
    assess = TRUE,
    dedupe = FALSE
  ))

  expect_named(built, c("raw", "derivative", "nobaseline"))
  expect_true(all(vapply(built, check_OpenSpecy, logical(1))))
  expect_true(all(c("material", "material_class", "material_type", "sn",
                    "assessment_flag", "assessment_issue_count",
                    "assessment_checks", "assessment_issues",
                    "assessment_potential_fixes") %in%
                  names(built$raw$metadata)))
  expect_equal(attr(built$derivative, "derivative_order"), "1")
  expect_equal(attr(built$nobaseline, "baseline"), "nobaseline")
  expect_true(built$raw$metadata$assessment_flag[1])
})

test_that("build_lib() skips metadata lookups with no shared key", {
  lib <- tiny_build_lib()
  no_shared <- data.table::data.table(
    missing_key = "not_present",
    joined_value = "skipped"
  )
  no_overlap <- data.table::data.table(
    source = "Z",
    joined_value = "skipped"
  )
  output_overlap <- data.table::data.table(
    source = c("A", "B", "C"),
    library_type = c("polymers", "polymers", "paints"),
    spectrum_type = c("ftir", "raman", "ftir")
  )
  ambiguous <- data.table::data.table(
    source = "A",
    sample_name = "s1",
    joined_value = "ambiguous"
  )

  expect_message(
    built <- build_lib(
      lib,
      recipes = list(raw = list()),
      metadata_lookups = no_shared,
      dedupe = FALSE,
      signal_noise = FALSE
    )$raw,
    "skipping metadata lookup 1/1"
  )
  expect_false("joined_value" %in% names(built$metadata))

  expect_message(
    built <- build_lib(
      lib,
      recipes = list(raw = list()),
      metadata_lookups = no_overlap,
      dedupe = FALSE,
      signal_noise = FALSE
    )$raw,
    "no usable shared key values"
  )
  expect_false("joined_value" %in% names(built$metadata))

  lib$metadata$library_type <- c("polymers", "paints", "polymers", "paints",
                                "polymers", "paints", "polymers", "paints")
  lib$metadata$spectrum_type <- "Raman"
  bad_utf8 <- rawToChar(as.raw(0xff))
  Encoding(bad_utf8) <- "UTF-8"
  lib$metadata$library_type[1] <- bad_utf8
  built <- build_lib(
    lib,
    recipes = list(raw = list()),
    metadata_lookups = output_overlap,
    dedupe = FALSE,
    signal_noise = FALSE,
    clean_metadata_values = TRUE,
    progress = FALSE
  )$raw
  expect_false(any(c("library_type.x", "library_type.y",
                     "spectrum_type.x", "spectrum_type.y") %in%
                     names(built$metadata)))
  expect_equal(built$metadata$library_type[1:3],
               c("polymers", "polymers", "paints"))
  expect_equal(built$metadata$spectrum_type[1:3],
               c("ftir", "raman", "ftir"))

  expect_error(
    build_lib(
      lib,
      recipes = list(raw = list()),
      metadata_lookups = ambiguous,
      dedupe = FALSE,
      signal_noise = FALSE,
      progress = FALSE
    ),
    "Candidate columns were"
  )
})

test_that("build_lib() standardizes source keys before external lookups", {
  lib <- filter_spec(tiny_build_lib(), 1:3)
  lib$metadata$organization <- c("source org", NA, NA)
  lib$metadata$user_name <- c("known", "fallback user", "unmapped")
  lib$metadata$library_type <- c("metadata type", NA, NA)
  source_lookup <- data.table::data.table(
    organization = c("source org", "fallback user"),
    library_type = c("organization type", "user type"),
    spectrum_type = "raman"
  )

  built <- suppressWarnings(build_lib(
    lib,
    recipes = list(raw = list()),
    metadata_lookups = list(
      lookup = source_lookup,
      by = "organization",
      fill_only = TRUE
    ),
    dedupe = FALSE,
    signal_noise = FALSE,
    progress = FALSE
  )$raw)

  expect_equal(built$metadata$library_type,
               c("metadata type", "user type", NA))
  expect_equal(built$metadata$spectrum_type, rep("ftir", 3))
  expect_equal(built$metadata$organization,
               c("source org", "fallback user", "unmapped"))
  expect_equal(built$metadata$library_name,
               c("source org", "fallback user", "unmapped"))
  expect_equal(
    attr(built, "metadata_lookup_reports")$canonical_source_keys[
      problem == "filled_canonical_key", n
    ],
    2L
  )
})

test_that("build_lib() cleans filename-derived spectrum identities and keys", {
  lib <- tiny_build_lib()
  lib$metadata$spectrum_identity <- c(
    "C:\\incoming\\Sample.CSV", "/tmp/Other.SPC",
    "relative/folder/Third.HDF5", "compound pe/pa/pe",
    "name.spc.csv", "plain identity", "opus.10", "unsupported.foo"
  )
  lookup <- data.table::data.table(
    spectrum_identity = c(
      "sample.csv", "other.spc", "third.hdf5", "compound pe/pa/pe",
      "name", "plain identity", "opus.10", "unsupported.foo"
    ),
    material = paste0("material_", seq_len(8))
  )

  built <- build_lib(
    lib,
    recipes = list(raw = list()),
    metadata_lookups = list(lookup = lookup, by = "spectrum_identity"),
    dedupe = FALSE,
    signal_noise = FALSE,
    clean_metadata_values = TRUE,
    progress = FALSE
  )$raw

  expect_identical(
    built$metadata$spectrum_identity,
    c("sample", "other", "third", "compound pe/pa/pe", "name",
      "plain identity", "opus", "unsupported.foo")
  )
  expect_identical(built$metadata$material, lookup$material)
  report <- attr(built, "spectrum_identity_cleanup_report")
  expect_s3_class(report, "data.table")
  expect_equal(sum(report$n), 5L)
  expect_true(all(c("original", "spectrum_identity", "n") %in%
                    names(report)))
})

test_that("build_lib() rejects lookup collisions after identity cleanup", {
  lib <- tiny_build_lib()
  lib$metadata$spectrum_identity <- rep("same", 8)
  lookup <- data.table::data.table(
    spectrum_identity = c("same.csv", "same.spc"),
    material = c("first", "second")
  )

  expect_error(
    build_lib(
      lib,
      recipes = list(raw = list()),
      metadata_lookups = list(lookup = lookup, by = "spectrum_identity"),
      dedupe = FALSE,
      signal_noise = FALSE,
      progress = FALSE
    ),
    "Lookup keys must be unique"
  )
})

test_that("prune_lib() orders classes, preserves floors, and audits removals", {
  wn <- seq(500, 3500, length.out = 80)
  shape_a <- dnorm(seq(-3, 3, length.out = length(wn)))
  shape_b <- dnorm(seq(-3, 3, length.out = length(wn)), mean = 1)
  spectra <- cbind(
    shape_a, shape_a * 1.01, shape_a * 0.99, shape_a + 0.002,
    shape_b,
    shape_b * 1.01, shape_b * 0.99, shape_b + 0.002,
    rev(shape_a), rev(shape_a) * 1.01
  )
  colnames(spectra) <- paste0("id", seq_len(ncol(spectra)))
  lib <- as_OpenSpecy(
    wn, spectra,
    metadata = data.table::data.table(
      sample_name = colnames(spectra),
      material_class = c(rep("large", 5), rep("medium", 3), rep("small", 2)),
      material_type = "plastic",
      spectrum_type = "ftir"
    )
  )

  report <- prune_lib(lib, min_n = 2, return = "report", progress = FALSE)

  expect_true(check_OpenSpecy(report$object))
  expect_equal(report$schedule$material_class, c("large", "medium", "small"))
  expect_equal(report$schedule$initial_n, c(5L, 3L, 2L))
  expect_true(nrow(report$removals) >= 1)
  expect_equal(report$summary$reassigned, 0L)
  expect_equal(nrow(report$excluded_classes), 0L)
  expect_named(
    report$excluded_classes,
    names(OpenSpecy:::.lib_prune_excluded_class_schema())
  )
  expect_true(all(table(report$object$metadata$material_class) >= 2))
  expect_identical(colnames(report$object$spectra),
                   report$object$metadata$sample_name)
  expect_identical(report$retained_ids, colnames(report$object$spectra))

  query <- which(lib$metadata$material_class == "large")
  candidates <- seq_len(ncol(lib$spectra))
  normalized <- OpenSpecy:::.lib_prune_normalize(
    lib$spectra, lib$wavenumber, c(2200, 2420)
  )
  legacy <- OpenSpecy:::.lib_prune_correlations(
    lib, query, candidates, c(2200, 2420), lib$metadata$sample_name
  )
  actual <- OpenSpecy:::.lib_prune_best_match(
    query, candidates, normalized, lib$metadata$sample_name,
    exclude_self = TRUE, block_size = 2L
  )
  bounded_full <- OpenSpecy:::.lib_prune_best_match(
    query, candidates, normalized, lib$metadata$sample_name,
    exclude_self = TRUE, block_size = length(query)
  )
  expect_equal(actual, bounded_full)

  # Proportional synthetic spectra create several effectively exact ties.
  # The bounded implementation deliberately resolves those ties by stable ID;
  # BLAS operation shape can make the legacy full matrix select a different
  # tied column. Verify that every bounded choice has the same scientific score.
  legacy_best <- matrixStats::rowMaxs(legacy)
  actual_columns <- match(actual$index, candidates)
  legacy_at_actual <- legacy[cbind(seq_along(query), actual_columns)]
  tolerance <- sqrt(.Machine$double.eps) * pmax(1, abs(legacy_best))
  expect_true(all(abs(legacy_at_actual - legacy_best) <= tolerance))
})

test_that("cross-class pruning closes internal conflicts before independent evidence", {
  normalized <- rbind(
    high = c(1, 0, 0, 0),
    b_l2 = c(1, 0, 0, 0),
    b_l3 = c(1, 0, 0, 0),
    tie_c = c(0, 1, 0, 0),
    tie_d = c(0, 1, 0, 0),
    same_source_e = c(0, 0, 1, 0),
    same_source_f = c(0, 0, 1, 0),
    generic = c(1, 0, 0, 0)
  )
  ids <- rownames(normalized)
  classes <- c("class_a", "class_b", "class_b", "class_c", "class_d",
               "class_e", "class_f", "other plastic")
  libraries <- c("lib_1", "lib_2", "lib_3", "lib_4", "lib_5",
                 "lib_6", "lib_6", "lib_7")
  pools <- rep("infrared", length(ids))

  first <- OpenSpecy:::.lib_prune_cross_class_conflicts(
    classes, pools, normalized, ids, libraries, threshold = 0.9,
    block_size = 2L
  )
  second <- OpenSpecy:::.lib_prune_cross_class_conflicts(
    classes, pools, normalized, ids, libraries, threshold = 0.9,
    block_size = 5L
  )

  expect_identical(first$removed_rows, c(1L, 4L, 5L, 6L, 7L))
  expect_equal(first$removals, second$removals)
  expect_equal(first$removals[spectrum_id == "high", evidence_libraries], 2L)
  expect_equal(first$removals[spectrum_id == "high", decision_round], 1L)
  expect_true(all(
    first$removals[spectrum_id %in% c("tie_c", "tie_d"), reason] ==
      "cross_class_equal_library_evidence"
  ))
  expect_true(all(
    first$removals[spectrum_id %in% c("same_source_e", "same_source_f"),
                   phase] == "internal"
  ))
  expect_true(all(
    first$removals[spectrum_id %in% c("same_source_e", "same_source_f"),
                   reason] == "cross_class_equal_internal_degree"
  ))
  expect_false("generic" %in% first$removals$spectrum_id)
})

test_that("internal cross-class majority removes the active high-degree spectrum", {
  normalized <- rbind(
    suspect = c(1, 0, 0, 0),
    reference_1 = c(1, 0, 0, 0),
    reference_2 = c(1, 0, 0, 0),
    reference_3 = c(1, 0, 0, 0),
    tie_a = c(0, 1, 0, 0),
    tie_b = c(0, 1, 0, 0)
  )
  first <- OpenSpecy:::.lib_prune_cross_class_conflicts(
    classes = c("class_a", rep("class_b", 3), "class_c", "class_d"),
    pools = rep("ftir", 6), normalized = normalized,
    ids = rownames(normalized), library_names = rep("one_library", 6),
    threshold = 0.9, min_n = 1, block_size = 2L
  )
  second <- OpenSpecy:::.lib_prune_cross_class_conflicts(
    classes = c("class_a", rep("class_b", 3), "class_c", "class_d"),
    pools = rep("ftir", 6), normalized = normalized,
    ids = rownames(normalized), library_names = rep("one_library", 6),
    threshold = 0.9, min_n = 1, block_size = 5L
  )

  expect_identical(first$removed_rows, c(1L, 5L, 6L))
  expect_equal(first$removals, second$removals)
  expect_equal(first$removals[spectrum_id == "suspect", active_degree], 3L)
  expect_equal(first$removals[spectrum_id == "suspect", decision_round], 1L)
  expect_true(all(first$removals[spectrum_id %in% c("tie_a", "tie_b"),
                                 decision_round] == 2L))
})

test_that("internal cross-class support counts span the complete database", {
  normalized <- rbind(
    a_source_1 = c(1, 0, 0, 0),
    b_source_1a = c(1, 0, 0, 0),
    b_source_1b = c(1, 0, 0, 0),
    a_source_2 = c(0, 0, 1, 0),
    a_source_3 = c(0, 0, 0, 1)
  )
  result <- OpenSpecy:::.lib_prune_cross_class_conflicts(
    classes = c("class_a", "class_b", "class_b", "class_a", "class_a"),
    pools = rep("ftir", 5), support_groups = rep("ftir", 5),
    normalized = normalized, ids = rownames(normalized),
    library_names = c("lib_1", "lib_1", "lib_1", "lib_2", "lib_3"),
    threshold = 0.9, min_n = 2
  )

  expect_identical(result$removed_rows, 1L)
  expect_equal(result$removals$class_n_before, 3L)
  expect_equal(result$removals$class_n_after, 2L)
  expect_equal(result$removals$review_status, "automatic")

  at_risk <- OpenSpecy:::.lib_prune_cross_class_conflicts(
    classes = c("class_a", "class_b", "class_b", "class_a", "class_a"),
    pools = rep("ftir", 5), support_groups = rep("ftir", 5),
    normalized = normalized, ids = rownames(normalized),
    library_names = c("lib_1", "lib_1", "lib_1", "lib_2", "lib_3"),
    threshold = 0.9, min_n = 3
  )
  expect_equal(at_risk$removals$review_status, "global_support_at_risk")
})

test_that("quarantine bundles retain spectra and review metadata", {
  lib <- tiny_build_lib()
  lib$metadata[, `:=`(
    material_class = rep(c("class_a", "class_b"), 4),
    material_type = "plastic", spectrum_type = "ftir",
    library_name = rep(c("lib_a", "lib_b"), 4)
  )]
  ids <- as.character(lib$metadata$sample_name)
  removal <- OpenSpecy:::.lib_prune_cross_class_schema()[0]
  removal <- data.table::data.table(
    phase = "internal", correlation_view = "typed_full",
    component_id = "internal:one", spectrum_id = ids[[1L]],
    prior_class = "class_a", library_name = "lib_a",
    matched_id = ids[[2L]], matched_class = "class_b",
    matched_library = "lib_b", correlation = 0.95, pool = "ftir",
    active_degree = 3L, matched_active_degree = 1L,
    evidence_libraries = 0L, conflicting_spectra = 3L,
    matched_evidence_libraries = 0L, decision_round = 1L,
    threshold = 0.9, class_n_before = 4L, class_n_after = 3L,
    schedule_order = NA_integer_, quarantine_status = "excluded",
    review_status = "automatic",
    reason = "cross_class_more_internal_conflicts"
  )
  part <- OpenSpecy:::.lib_quarantine_part(
    lib, removal, "derivative", "ftir", min_n = 3
  )
  bundle <- OpenSpecy:::.lib_quarantine_bundle(
    list(part), build_signature = "fixture", thresholds = 0.9
  )
  output <- tempfile("OpenSpecy-quarantine-review-")
  dir.create(output)
  on.exit(unlink(output, recursive = TRUE, force = TRUE), add = TRUE)
  OpenSpecy:::.lib_write_quarantine_review(bundle, output)
  restored <- readRDS(file.path(output, "review", "quarantined_spectra.rds"))

  expect_s3_class(restored, "OpenSpecyQuarantine")
  expect_named(restored, c("spectra", "conflicts", "manifest"))
  expect_true(check_OpenSpecy(restored$spectra[["derivative/ftir"]]))
  expect_equal(
    restored$spectra[["derivative/ftir"]]$metadata$quarantine_reason,
    "cross_class_more_internal_conflicts"
  )
  expect_true(all(file.exists(file.path(
    output, "review",
    c("quarantined_spectra_metadata.csv",
      "quarantined_spectra_conflicts.csv")
  ))))
})

test_that("typed closure filters parents before medoid construction", {
  wn <- seq(800, 3200, length.out = 60)
  peak <- dnorm(seq(-3, 3, length.out = length(wn)))
  other_1 <- dnorm(seq(-3, 3, length.out = length(wn)), mean = -1.5)
  other_2 <- dnorm(seq(-3, 3, length.out = length(wn)), mean = 1.5)
  spectra <- cbind(peak, peak * 1.01, peak * 0.99, other_1, other_2)
  colnames(spectra) <- c("suspect", "b1", "b2", "a2", "a3")
  parent <- as_OpenSpecy(
    wn, spectra,
    metadata = data.table::data.table(
      sample_name = colnames(spectra),
      material_class = c("class_a", "class_b", "class_b", "class_a", "class_a"),
      material_type = "plastic", spectrum_type = "ftir",
      library_name = c("lib_1", "lib_1", "lib_1", "lib_2", "lib_3")
    )
  )
  closed <- OpenSpecy:::.lib_close_typed_reference_libraries(
    list(derivative = list(ftir = parent)),
    prune_spec = list(derivative = list(
      cross_class = TRUE, cross_class_threshold = 0.9, min_n = 2
    )),
    progress = FALSE
  )

  expect_false("suspect" %in%
                 closed$libraries$derivative$ftir$metadata$sample_name)
  expect_equal(unname(table(
    closed$libraries$derivative$ftir$metadata$material_class
  )["class_a"]), 2L)
  expect_true(nrow(closed$removals) >= 1L)
  expect_true(any(closed$removals$correlation_view == "typed_full"))
  expect_length(closed$quarantine_parts, 1L)
  expect_true(check_OpenSpecy(closed$quarantine_parts[[1L]]))
})

test_that("cross-class pruning uses a strict threshold", {
  normalized <- rbind(c(1, 0), c(0.9, sqrt(1 - 0.9^2)))
  args <- list(
    classes = c("class_a", "class_b"), pools = c("infrared", "infrared"),
    normalized = normalized, ids = c("a", "b"),
    library_names = c("lib_a", "lib_b")
  )

  exact <- do.call(
    OpenSpecy:::.lib_prune_cross_class_conflicts,
    c(args, list(threshold = 0.9))
  )
  below <- do.call(
    OpenSpecy:::.lib_prune_cross_class_conflicts,
    c(args, list(threshold = 0.899))
  )

  expect_length(exact$removed_rows, 0L)
  expect_identical(below$removed_rows, 1:2)
})

test_that("prune_lib() applies cross-class removal before generic reassignment", {
  wn <- seq(500, 3500, length.out = 80)
  shape <- dnorm(seq(-3, 3, length.out = length(wn)))
  spectra <- cbind(shape, shape * 1.01, shape * 0.99, shape * 1.02)
  colnames(spectra) <- c("suspect", "reference_1", "reference_2", "generic")
  lib <- as_OpenSpecy(
    wn, spectra,
    metadata = data.table::data.table(
      sample_name = colnames(spectra),
      material_class = c("class_a", "class_b", "class_b", "other plastic"),
      material_type = "plastic", spectrum_type = "ftir",
      library_name = c("lib_1", "lib_2", "lib_3", "lib_4")
    )
  )
  attr(lib, "test_marker") <- "preserved"

  report <- prune_lib(
    lib, min_n = 1, cross_class = TRUE, cross_class_threshold = 0.9,
    return = "report", progress = FALSE
  )

  expect_identical(report$cross_class_removals$spectrum_id, "suspect")
  expect_equal(report$summary$cross_class_removed, 1L)
  expect_false("suspect" %in% report$retained_ids)
  expect_equal(
    report$object$metadata[sample_name == "generic", material_class],
    "class_b"
  )
  expect_identical(colnames(report$object$spectra),
                   report$object$metadata$sample_name)
  expect_identical(report$object$wavenumber, lib$wavenumber)
  expect_identical(attr(report$object, "test_marker"), "preserved")
  expect_equal(report$object$spectra,
               lib$spectra[, report$retained_ids, drop = FALSE])

  recovered <- OpenSpecy:::.lib_recover_library_assessments(
    list(derivative = report$object), list()
  )
  expect_equal(recovered$pruning_removals$spectrum_id, "suspect")
})

test_that("prune_lib() validates cross-class policy inputs", {
  lib <- tiny_build_lib()
  lib$metadata$material_type <- "plastic"

  expect_error(prune_lib(lib, cross_class = NA), "TRUE or FALSE")
  expect_error(prune_lib(lib, cross_class_threshold = 1.01), "in \\[0, 1\\]")
  expect_error(
    prune_lib(lib, cross_class = TRUE),
    "requires populated library_name"
  )
})

test_that("prune_lib() reassigns undersupported classes by spectrum type", {
  wn <- seq(500, 3500, length.out = 40)
  spectra <- vapply(seq_len(7), function(i) {
    dnorm(seq(-3, 3, length.out = length(wn)), mean = i / 20)
  }, numeric(length(wn)))
  colnames(spectra) <- c(
    "ftir_a1", "ftir_a2", "ftir_rare", "raman_rare1", "raman_rare2",
    "ftir_unclassified", "ftir_missing"
  )
  lib <- as_OpenSpecy(
    wn, spectra,
    metadata = data.table::data.table(
      sample_name = colnames(spectra),
      material_class = c(
        "stable", "stable", "rare", "rare", "rare", "unclassified", NA
      ),
      material_type = "plastic",
      spectrum_type = c(rep("ftir", 3), rep("raman", 2), "ftir", "ftir")
    )
  )

  messages <- capture.output(
    report <- prune_lib(
      lib, min_n = 2, return = "report", progress = TRUE
    ),
    type = "message"
  )
  expect_match(
    paste(messages, collapse = "\n"),
    "minimum support gate reassigned 1 and removed 1 spectrum/spectra"
  )

  expect_equal(
    report$excluded_classes[, c(
      "spectrum_type", "material_class", "observed_n", "minimum_spectra",
      "shortfall", "destination_class", "spectra_removed", "action", "reason"
    ), with = FALSE],
    data.table::data.table(
      spectrum_type = c("ftir", "ftir"),
      material_class = c("rare", "unclassified"),
      observed_n = c(1L, 1L), minimum_spectra = c(2L, 2L),
      shortfall = c(1L, 1L), destination_class = c("stable", NA_character_),
      spectra_removed = c(0L, 1L), action = c("reassigned", "dropped"),
      reason = c("class_below_min_n_reassigned",
                 "class_below_min_n_no_eligible_destination")
    )
  )
  expect_true(all(c("raman_rare1", "raman_rare2") %in% report$retained_ids))
  expect_true("ftir_missing" %in% report$retained_ids)
  expect_true("ftir_rare" %in% report$retained_ids)
  expect_false("ftir_unclassified" %in% report$retained_ids)
  expect_equal(
    report$object$metadata[sample_name == "ftir_rare", material_class],
    "stable"
  )
  expect_equal(
    report$reassignments[spectrum_id == "ftir_rare", reason],
    "class_below_min_n_reassigned"
  )
  threshold_removals <- report$removals[reason == "class_below_min_n"]
  expect_equal(threshold_removals$spectrum_id, "ftir_unclassified")
  expect_true(all(is.na(threshold_removals$matched_id)))
  expect_type(threshold_removals$matched_id, "character")
  expect_equal(report$summary$classes_excluded, 1L)
  expect_equal(report$summary$threshold_removed, 1L)
  expect_true(check_OpenSpecy(report$object))
  expect_identical(colnames(report$object$spectra),
                   report$object$metadata$sample_name)

  recovered <- OpenSpecy:::.lib_recover_library_assessments(
    list(raw = lib, derivative = report$object),
    list(metadata_drop = data.table::data.table(metadata_column = character()))
  )
  expect_named(
    recovered$pruning_excluded_classes,
    names(OpenSpecy:::.lib_prune_excluded_assessment_schema())
  )
  expect_equal(unique(recovered$pruning_excluded_classes$artifact),
               "derivative")
  expect_equal(nrow(recovered$pruning_excluded_classes), 2L)
})

test_that("prune_lib() resolves generic labels before minimum support", {
  wn <- seq(500, 3500, length.out = 40)
  shape <- dnorm(seq(-3, 3, length.out = length(wn)))
  spectra <- cbind(shape, shape * 1.01)
  colnames(spectra) <- c("rare", "generic")
  lib <- as_OpenSpecy(
    wn, spectra,
    metadata = data.table::data.table(
      sample_name = colnames(spectra),
      material_class = c("polymer rare", "other plastic"),
      material_type = "plastic", spectrum_type = "ftir"
    )
  )

  report <- prune_lib(lib, min_n = 2, return = "report", progress = FALSE)

  expect_equal(report$object$metadata$material_class,
               rep("polymer rare", 2))
  expect_equal(nrow(report$reassignments), 1L)
  expect_equal(nrow(report$excluded_classes), 0L)
  expect_equal(report$schedule$initial_n, 2L)
})

test_that("prune_lib() sends an undersupported class to one correlated class", {
  wn <- seq(500, 3500, length.out = 40)
  shape_a <- dnorm(seq(-3, 3, length.out = length(wn)), mean = -0.5)
  shape_b <- dnorm(seq(-3, 3, length.out = length(wn)), mean = 1)
  spectra <- cbind(
    shape_a, shape_a * 1.01, shape_a * 0.99,
    shape_b, shape_b * 1.01, shape_b * 0.99,
    shape_a * 1.02, shape_a * 0.98
  )
  colnames(spectra) <- paste0("whole_", seq_len(ncol(spectra)))
  lib <- as_OpenSpecy(
    wn, spectra,
    metadata = data.table::data.table(
      sample_name = colnames(spectra),
      material_class = c(rep("class_a", 3), rep("class_b", 3), rep("rare", 2)),
      material_type = "plastic", spectrum_type = "ftir"
    )
  )

  report <- prune_lib(lib, min_n = 3, return = "report", progress = FALSE)
  reassigned <- report$reassignments[prior_class == "rare"]

  expect_equal(nrow(reassigned), 2L)
  expect_identical(unique(reassigned$material_class), "class_a")
  expect_true(all(report$object$metadata$sample_name %in%
                    colnames(lib$spectra)))
  expect_false("rare" %in% report$object$metadata$material_class)
  expect_equal(
    report$excluded_classes[material_class == "rare", action],
    "reassigned"
  )
})

test_that("prune_lib() retains unclassified spectra outside matching", {
  lib <- tiny_build_lib()
  lib$metadata$material_type <- "plastic"
  lib$metadata$material_class[1:2] <- "unclassified"
  protected_ids <- lib$metadata$sample_name[1:2]

  report <- prune_lib(lib, min_n = 1, return = "report", progress = FALSE)

  expect_true(all(protected_ids %in% report$retained_ids))
  expect_false("unclassified" %in% report$schedule$material_class)
  expect_false(any(report$removals$prior_class == "unclassified"))
  expect_false(any(report$removals$matched_class == "unclassified"))
})

test_that("prune_lib() reassigns generic classes and tolerates no candidates", {
  lib <- filter_spec(tiny_build_lib(), 1:4)
  lib$metadata$material_class <- c("polymer a", "other plastic",
                                   "other material", "other plastic")
  lib$metadata$material_type <- c("plastic", "plastic", "mineral", "plastic")
  lib$metadata$spectrum_type <- c("ftir", "ftir", "raman", NA)
  report <- prune_lib(lib, min_n = 1, return = "report", progress = FALSE)

  expect_equal(report$object$metadata$material_class[2], "polymer a")
  expect_equal(report$object$metadata$material_class[3], "other material")
  expect_equal(report$object$metadata$material_class[4], "other plastic")
  expect_equal(nrow(report$reassignments), 1)
  expect_equal(report$summary$removed, 0L)
})

test_that("prune_lib() constrains generic reassignment and updates type", {
  wn <- seq(500, 3500, length.out = 80)
  plastic <- dnorm(seq(-3, 3, length.out = length(wn)))
  organic <- dnorm(seq(-3, 3, length.out = length(wn)), mean = 1)
  mineral <- rev(cumsum(seq_along(wn)))
  spectra <- cbind(plastic, organic, mineral, plastic, organic, mineral)
  colnames(spectra) <- paste0("generic", seq_len(ncol(spectra)))
  lib <- as_OpenSpecy(
    wn, spectra,
    metadata = data.table::data.table(
      sample_name = colnames(spectra),
      material_class = c(
        "polymer a", "organic matter", "mineral",
        "other plastic", "other material", "other"
      ),
      material_type = c(
        "plastic", "not plastic", "not plastic", "plastic", "not plastic",
        "other"
      ),
      spectrum_type = "ftir"
    )
  )

  report <- prune_lib(lib, min_n = 1, return = "report", progress = FALSE)

  expect_equal(
    report$object$metadata$material_class[4:6],
    c("polymer a", "organic matter", "mineral")
  )
  expect_equal(
    report$object$metadata$material_type[4:6],
    c("plastic", "not plastic", "not plastic")
  )
  expect_equal(nrow(report$reassignments), 3L)
  expect_true(all(report$reassignments$reason == "nearest_eligible_class"))
})

test_that("prune_lib() preserves non-generic class labels and empty audits", {
  lib <- filter_spec(tiny_build_lib(), 1:2)
  lib$metadata$material_class <- c("Polymer A", "Polymer A")
  lib$metadata$material_type <- "plastic"
  report <- prune_lib(lib, min_n = 1, return = "report", progress = FALSE)

  expect_equal(report$object$metadata$material_class,
               c("Polymer A", "Polymer A"))
  expect_s3_class(report$reassignments, "data.table")
  expect_s3_class(report$removals, "data.table")
  expect_equal(nrow(report$reassignments), 0)
  expect_equal(nrow(report$removals), 0)
  expect_equal(report$summary$reassigned, 0L)
  expect_equal(report$summary$removed, 0L)
})

test_that("build_lib() applies pruning only to named recipes", {
  lib <- tiny_build_lib()
  lib$metadata$material_type <- "plastic"
  built <- build_lib(
    lib,
    recipes = list(raw = list(), processed = list()),
    prune = list(processed = list(min_n = 1, progress = FALSE)),
    dedupe = FALSE,
    signal_noise = FALSE,
    progress = FALSE
  )

  expect_null(attr(built$raw, "prune_report"))
  expect_true(is.list(attr(built$processed, "prune_report")))
})

test_that("reference workflow tables encode reviewed taxonomy and source rules", {
  classes <- data.table::fread(
    reference_workflow_data_path("classes_reference.csv")
  )
  regex_classes <- data.table::fread(
    reference_workflow_data_path("classes_regex.csv")
  )
  hierarchy <- data.table::fread(
    reference_workflow_data_path("material_hierarchy.csv")
  )
  types <- data.table::fread(
    reference_workflow_data_path("library_types.csv")
  )
  drops <- data.table::fread(
    reference_workflow_data_path("metadata_drop_columns.csv")
  )
  known_bad <- data.table::fread(
    reference_workflow_data_path("known_bad_ids.csv")
  )
  form_regex <- data.table::fread(
    reference_workflow_data_path("material_form_regex.csv")
  )
  common_use <- data.table::fread(
    reference_workflow_data_path("common_use_reference.csv")
  )

  expect_false(anyNA(classes$spectrum_identity))
  expect_false(any(classes$spectrum_identity == ""))
  expect_identical(anyDuplicated(classes$spectrum_identity), 0L)
  expect_false(any(grepl(
    "^([0-9]{1,2}-[A-Za-z]{3}|[A-Za-z]{3}-[0-9]{1,2})$",
    classes$spectrum_identity
  )))
  expect_false(any(grepl("^regex:", classes$spectrum_identity)))
  expect_named(regex_classes, c("pattern", "material"))
  expect_false(anyNA(regex_classes$pattern))
  expect_false(any(regex_classes$pattern == ""))
  expect_identical(anyDuplicated(regex_classes$pattern), 0L)
  class_audit <- predict_class_reference(
    classes, regex_classes, return = "report"
  )
  expect_gte(class_audit$summary$predicted, 0L)
  expect_equal(class_audit$summary$clashes, 0L)
  expect_gt(class_audit$summary$overlaps, 0L)
  expect_true(all(!is.na(regex_classes$material)))
  expect_identical(anyDuplicated(known_bad$sample_name), 0L)
  expect_true(all(grepl("^[[:xdigit:]]{32}$", known_bad$sample_name)))
  expect_identical(anyDuplicated(hierarchy$material), 0L)
  expect_silent(OpenSpecy:::.lib_validate_material_form_regex(form_regex))
  expect_silent(OpenSpecy:::.lib_validate_common_use_reference(
    common_use, hierarchy
  ))
  expect_setequal(
    common_use$material_class, unique(hierarchy$material_class)
  )
  expect_true(all(c("consumer", "industrial", "mixed") %in%
                    stats::na.omit(common_use$common_use)))
  expect_equal(hierarchy[material == "other", material_class], "other")
  expect_equal(hierarchy[material == "other", material_type], "other")
  expect_equal(classes[spectrum_identity == "pa", material], "polyamides")
  exact_classes <- classes[!grepl("^regex:", spectrum_identity)]
  expect_identical(
    OpenSpecy:::.lib_clean_spectrum_identity(exact_classes$spectrum_identity),
    exact_classes$spectrum_identity
  )
  expect_true(all(
    hierarchy[grepl("adipate", material), material_class] == "polyesters"
  ))
  expect_false("polyamides (polylactams)" %in% hierarchy$material_class)
  expect_true(all(c("polyamides", "polyacrylamides") %in%
                    hierarchy$material_class))
  expect_equal(
    classes[spectrum_identity == "plc004_kn95 outer layer_pp", material],
    "poly(propylene)"
  )
  expect_equal(
    classes[spectrum_identity == "plc008_label tape_unknown", material],
    "other plastic"
  )
  expect_true(all(
    classes[spectrum_identity %in% c(
      "crumb rubber from used tires", "r 4. black bridgestone tire fragment",
      "r 5. black michelin tire fragment", "r 6. black bike tire fragment",
      "r 8. white bike tire fragment"
    ), material] == "styrene-butadiene"
  ))
  expect_equal(
    classes[spectrum_identity == "poly(ethylene:propylene:diene)", material],
    "epdm rubber (ethylene propylene diene monomer rubber)"
  )
  expect_equal(
    classes[spectrum_identity == "lahmian medium acrylic paint", material],
    "polyacrylates"
  )
  expect_true(all(
    classes[spectrum_identity %in% c("alkyd varnish", "alkyd_varnish"),
            material] == "polyesters"
  ))
  binder_classes <- c(
    "acrylic paint" = "polyacrylates",
    "acrylic-alkyd paint" = "polyesters",
    "acrylic-epoxy paint" = "polydiglycidyl ethers",
    "acrylic-urethane paint" = "polyurethanes",
    "alkyd paint" = "polyesters",
    "chlorosulfonated polyethylene paint" = "polyhaloolefins",
    "ethylene-acrylate paint" = "polyolefins",
    "nitrocellulose paint" = "polycellulose derivatives",
    "other paint" = "other plastic",
    "polyester paint" = "polyesters",
    "polyvinyl formal paint" = "polyvinylalcohols",
    "styrene-acrylic paint" = "polyacrylates",
    "urethane paint" = "polyurethanes",
    "urethane-alkyd paint" = "polyesters",
    "vinyl acetate paint" = "polyvinylesters",
    "vinyl chloride paint" = "polyhaloolefins",
    "vinyl chloride-vinyl acetate paint" = "polyhaloolefins",
    "vinyl-acrylic paint" = "polyacrylates"
  )
  actual_binders <- classes[
    spectrum_identity %in% names(binder_classes),
    stats::setNames(material, spectrum_identity)
  ]
  expect_identical(unname(actual_binders[names(binder_classes)]),
                   unname(binder_classes))
  paint_classes <- c("paint", "acrylic paint", "alkyd paint", "urethane paint")
  expect_false(any(hierarchy$material_class %in% paint_classes))
  expect_false(any(common_use$material_class %in% paint_classes))
  organic_recommendations <- c(
    "1,5-pentanediol", "11-aminoundecanoic acid", "1-bromobutane",
    "1-vinyl-2-pyrolidinone", "2-bromopropanoic acid", "2-butanone",
    "2-chloro-4-methylpentane", "2-methyl-2-pentanol", "butyl acrylate",
    "ethyl (a-chloromethyl)acrylate", "ethyl acrylate",
    "n,n-diethyl-m-toluamide", "n,n-dimethyl-m-toluamide",
    "n-butyl benzoate", "n-decyl methacrylate", "n-vinylformamide",
    "p-phenylenediamine", "p-vinylbenzyl chloride", "p-xylene",
    "propionic acid", "sec-butyl benzoate", "styrene",
    "tamoxifen (lot #bcbt3163)"
  )
  expect_true(all(
    classes[spectrum_identity %in% organic_recommendations, material] ==
      "organic matter"
  ))
  expect_true(all(
    classes[spectrum_identity %in% c(
      "c2. blue fiber", "c4. green fiber", "c6. red fiber",
      "c8. pink fiber bundle", "c9. grey fiber"
    ), material] == "other"
  ))
  expect_equal(
    classes[spectrum_identity == "fibre_polyamide_6_p6", material],
    "nylon 6,6 - poly(hexamethylene adipamide)"
  )
  expect_equal(
    classes[spectrum_identity == "ps 16. purple lego fragment", material],
    "acrylonitrile butadiene styrene (abs)"
  )
  expect_true(all(
    classes[spectrum_identity %in% c("polyesterurethane", "polyetherurethane"),
            material] == "polyurethanes"
  ))
  expect_true(all(
    classes[spectrum_identity %in% c("tylose", "tylose2"), material] ==
      "methyl cellulose"
  ))
  expect_true(all(c("microplastix", "nist", "hcmr", "cnr", "vliz",
                    "nicolas coca") %in% types$organization))
  expect_false("user_name" %in% names(types))
  expect_equal(types[organization == "nist", spectrum_type], "nir")
  expect_equal(
    types[organization == "monterey bay aquarium research institute",
          spectrum_type],
    "raman"
  )
  expect_equal(
    types[organization == "walters art museum pigment library",
          c(library_type, spectrum_type)],
    c("pigments", "raman")
  )
  expect_false(anyNA(types$spectrum_type))
  expect_false(any(types$spectrum_type == ""))
  expect_true(all(c("interpretation", "form_factor", "shape", "x_unit",
                    "spectrumid", "locationdescription", "v1",
                    "3997_91411", "polymer_hit_3_labs") %in%
                  drops$metadata_column))
})

test_that("reference metadata enrichment is ordered and conservative", {
  form_regex <- data.table::fread(
    reference_workflow_data_path("material_form_regex.csv")
  )
  common_use <- data.table::fread(
    reference_workflow_data_path("common_use_reference.csv")
  )
  metadata <- data.table::data.table(
    sample_name = paste0("s", seq_len(7)),
    spectrum_identity = c(
      "paint chip", "clear plastic film", "fiber pellet", "profile edge",
      "hard plastic", "EPDM gasket", "tyre fragment"
    ),
    material_form = c("rubber", rep(NA_character_, 6)),
    notes = c("paint", "sheet", "fiber and pellet", "filmography",
              "rigid plastic", "seal", "road wear"),
    material_class = c(
      "poly(styrene-butadiene) rubber", "polyethylene", "polypropylene",
      "other plastic", "polyethylene",
      "poly(ethylene-propylene-diene) rubber",
      "poly(styrene-butadiene) rubber"
    )
  )

  form <- OpenSpecy:::.lib_enrich_material_form(metadata, form_regex)
  expect_identical(
    form$data$material_form,
    c("rubber", "film plastic", NA, NA, "hard plastic", "rubber", NA)
  )
  expect_equal(form$summary[metric == "conflicting", value], 2L)
  expect_equal(form$summary[metric == "unmatched", value], 1L)
  expect_true(all(c("matched_patterns", "evidence", "conflict") %in%
                    names(form$matches)))
  expect_match(form$matches[row_id == 1L, evidence], "material_form=rubber")

  expect_silent(empty_form <- OpenSpecy:::.lib_enrich_material_form(
    data.table::data.table(spectrum_identity = "profile"), form_regex
  ))
  expect_identical(empty_form$summary$metric,
                   c("total", "populated", "unmatched", "conflicting"))

  use <- OpenSpecy:::.lib_enrich_common_use(form$data, common_use)
  expect_identical(
    use$data$common_use,
    c("mixed", "consumer", "consumer", NA, "consumer", "mixed", "mixed")
  )
  expect_true(all(c(
    "consumer_mass_share", "industrial_mass_share", "source_url",
    "retrieved_date"
  ) %in% names(use$coverage)))
  expect_silent(empty_use <- OpenSpecy:::.lib_enrich_common_use(
    data.table::data.table(material_class = "other plastic"), common_use
  ))
  expect_identical(empty_use$summary$metric,
                   c("total", "populated", "unmatched"))

  mixed_hierarchy <- data.table::data.table(
    material = "example", material_class = "example", material_type = "plastic"
  )
  mixed_reference <- data.table::data.table(
    material_class = "example", common_use = "mixed",
    consumer_mass_share = 0.4, industrial_mass_share = 0.4,
    unclassified_mass_share = 0.2, basis_year = 2020,
    geography = "global", source_url = "https://example.org/source",
    retrieved_date = "2026-09-29",
    evidence_notes = "review fixture"
  )
  expect_silent(OpenSpecy:::.lib_validate_common_use_reference(
    mixed_reference, mixed_hierarchy
  ))
  mixed_reference$consumer_mass_share <- 0.2
  expect_error(
    OpenSpecy:::.lib_validate_common_use_reference(
      mixed_reference, mixed_hierarchy
    ),
    "valid threshold shares"
  )
  qualitative_reference <- data.table::copy(mixed_reference)
  qualitative_reference[, `:=`(
    consumer_mass_share = NA_real_, industrial_mass_share = NA_real_,
    unclassified_mass_share = NA_real_, basis_year = NA_integer_,
    geography = "general"
  )]
  expect_silent(OpenSpecy:::.lib_validate_common_use_reference(
    qualitative_reference, mixed_hierarchy
  ))
  qualitative_reference$source_url <- NA_character_
  expect_error(
    OpenSpecy:::.lib_validate_common_use_reference(
      qualitative_reference, mixed_hierarchy
    ),
    "source provenance"
  )
  partial_reference <- data.table::copy(qualitative_reference)
  partial_reference$source_url <- "https://example.org/source"
  partial_reference$consumer_mass_share <- 0.6
  expect_error(
    OpenSpecy:::.lib_validate_common_use_reference(
      partial_reference, mixed_hierarchy
    ),
    "either all missing or all present"
  )
})

test_that("predict_class_reference() fills only blanks and audits overlaps", {
  classes <- data.table::data.table(
    spectrum_identity = c(
      "nylon exact override", "nylon fiber", "nylon blend", "unknown"
    ),
    material = c(
      "manual material", NA_character_, NA_character_, NA_character_
    )
  )
  rules <- data.table::data.table(
    pattern = c("^nylon", "blend$"),
    material = c("polyamides", "polyesters")
  )

  report <- predict_class_reference(classes, rules, return = "report")
  exact <- report$data

  expect_equal(exact[spectrum_identity == "nylon exact override", material],
               "manual material")
  expect_equal(exact[spectrum_identity == "nylon fiber", material],
               "polyamides")
  expect_true(is.na(exact[spectrum_identity == "nylon blend", material]))
  expect_true(is.na(exact[spectrum_identity == "unknown", material]))
  expect_equal(report$summary$predicted, 1L)
  expect_equal(report$summary$clashes, 1L)
  expect_equal(report$summary$unmatched, 2L)
  expect_equal(report$summary$overlaps, 1L)
  expect_equal(report$clashes$spectrum_identity, "nylon blend")
  expect_match(report$clashes$materials, "polyamides")
  expect_match(report$clashes$materials, "polyesters")
  expect_equal(report$overlaps$spectrum_identity, "nylon exact override")
  expect_false(report$overlaps$agreement)
  expect_equal(report$predictions$spectrum_identity, "nylon fiber")
})

test_that("reference class completion resolves reviewed wrappers and audits uncertainty", {
  lib <- tiny_build_lib()
  lib$metadata[, `:=`(
    spectrum_identity = c(
      "known", "pa_ref.csv", "mffrc001_nylon (pa6)_maker.0",
      "cellulose_like_ref.csv", NA_character_, "known_2", "known_3",
      "known_4"
    ),
    user_name = c(
      NA_character_, "gicquel et al. 2024",
      "elise granek and kellie teague", "gicquel et al. 2024",
      NA_character_, NA_character_, NA_character_, NA_character_
    ),
    material = NA_character_,
    material_type = NA_character_
  )]
  lib$metadata$material_class <- c(
    "known class", rep(NA_character_, 3), rep("known class", 4)
  )
  classes <- data.table::data.table(
    spectrum_identity = c("pa", "nylon", "cellulose_like"),
    material = c("polyamides", "polyamides", "organic matter")
  )
  hierarchy <- data.table::data.table(
    material = c("nylon 6 - poly(caprolactam)", "organic matter"),
    material_class = c("polyamides", "organic matter"),
    material_type = c("plastic", "not plastic")
  )

  completed <- .lib_complete_reference_classes(lib, classes, hierarchy)
  report <- attr(completed, "class_coverage_report")

  expect_true(check_OpenSpecy(completed))
  expect_false(anyNA(completed$metadata$material_class))
  expect_equal(completed$metadata$material_class[2:3],
               rep("polyamides", 2))
  expect_equal(completed$metadata$class_lookup_key[2:3], c("pa", "nylon"))
  expect_equal(completed$metadata$class_assignment_reason[2:3],
               rep("reviewed_source_key", 2))
  expect_equal(completed$metadata$material_class[4], "organic matter")
  expect_equal(completed$metadata$class_assignment_reason[4],
               "reviewed_source_key")
  expect_identical(completed$metadata$spectrum_identity,
                   lib$metadata$spectrum_identity)
  expect_equal(report[stage == "after", populated_class], nrow(lib$metadata))
  expect_equal(report[stage == "after", reviewed_source_key], 3L)
  expect_equal(report[stage == "after", unclassified], 0L)
  expect_equal(report[stage == "after", other], 0L)
})

test_that("reference class completion labels and caps unresolved other", {
  source <- tiny_build_lib()
  ids <- paste0("coverage", seq_len(100L))
  spectra <- matrix(
    rep(source$spectra[, 1L], 100L), nrow = nrow(source$spectra)
  )
  colnames(spectra) <- ids
  metadata <- data.table::data.table(
    sample_name = ids, col_id = ids,
    spectrum_identity = c(rep("known", 99L), "unknown"),
    user_name = NA_character_,
    material = c(rep("known", 99L), NA_character_),
    material_class = c(rep("known class", 99L), NA_character_),
    material_type = c(rep("plastic", 99L), NA_character_),
    spectrum_type = "ftir"
  )
  lib <- as_OpenSpecy(source$wavenumber, spectra, metadata = metadata)
  classes <- data.table::data.table(
    spectrum_identity = character(), material = character()
  )
  hierarchy <- data.table::data.table(
    material = "known", material_class = "known class",
    material_type = "plastic"
  )

  completed <- .lib_complete_reference_classes(lib, classes, hierarchy)
  report <- attr(completed, "class_coverage_report")
  expect_equal(completed$metadata$material[100L], "other")
  expect_equal(completed$metadata$material_class[100L], "other")
  expect_equal(completed$metadata$material_type[100L], "other")
  expect_equal(report[stage == "after", unresolved_other], 1L)
  expect_equal(report[stage == "after", other_fraction], 0.01)

  lib$metadata[99:100, `:=`(
    spectrum_identity = c("unknown_2", "unknown"),
    material = NA_character_, material_class = NA_character_,
    material_type = NA_character_
  )]
  expect_error(
    .lib_complete_reference_classes(lib, classes, hierarchy),
    "above the reviewed 1% maximum",
    fixed = TRUE
  )
})

test_that("official other policy removes unresolved rows and retains broad classes", {
  lib <- tiny_build_lib()
  lib$metadata[, `:=`(
    spectrum_identity = c(NA_character_, paste0("identity_", 2:8)),
    sample_id = paste0("sample_", 1:8),
    file_name = paste0("file_", 1:8, ".csv"),
    organization = source,
    user_name = source,
    citation = "citation",
    other_info = "note",
    material = c("other", "other plastic", "polyamide", rep("polyester", 5)),
    material_type = c("other", "plastic", "other material", rep("plastic", 5))
  )]
  lib$metadata$material_class <- c(
    "other", "other plastic", "other material", rep("polyesters", 5)
  )

  removed <- OpenSpecy:::.lib_apply_other_policy(
    list(raw = lib), remove_other = TRUE, report = NULL
  )
  expect_identical(colnames(removed$libraries$raw$spectra), paste0("s", 2:8))
  expect_identical(nrow(removed$libraries$raw$metadata), 7L)
  expect_named(removed$review, names(OpenSpecy:::.lib_other_review_schema()))
  expect_named(removed$summary, names(OpenSpecy:::.lib_other_filter_schema()))
  expect_type(removed$review$source_row, "integer")
  expect_type(removed$summary$removed, "integer")
  expect_equal(removed$review$reason,
               c("missing_spectrum_identity", rep("reviewed_broad_category", 2)))
  expect_equal(
    removed$review$action,
    c("removed", rep("retained_reviewed_broad_category", 2))
  )
  expect_equal(removed$summary[, .(candidates, removed, after)],
               data.table::data.table(candidates = 3L, removed = 1L, after = 7L))
  expect_true(check_OpenSpecy(removed$libraries$raw))

  retained <- OpenSpecy:::.lib_apply_other_policy(
    list(raw = lib), remove_other = FALSE, report = NULL
  )
  expect_identical(ncol(retained$libraries$raw$spectra), 8L)
  expect_true(all(
    retained$review$action == "retained_for_semisupervised_reassignment"
  ))
  expect_identical(retained$summary$removed, 0L)
  expect_identical(formals(build_lib)$remove_other, TRUE)
})

test_that("source-library retention reports complete drops with a stage reason", {
  stages <- list(
    data.table::data.table(
      stage = "prepared", artifact = "raw",
      library_name = c("kept", "dropped"), spectra = c(4L, 2L)
    ),
    data.table::data.table(
      stage = "post_exclusion", artifact = "raw",
      library_name = c("kept", "dropped"), spectra = c(4L, 2L)
    ),
    data.table::data.table(
      stage = "core", artifact = "raw",
      library_name = c("kept", "dropped"), spectra = c(4L, 2L)
    ),
    data.table::data.table(
      stage = "post_other", artifact = "raw",
      library_name = "kept", spectra = 4L
    ),
    data.table::data.table(
      stage = c("post_quality", "post_prune", "post_transform", "final"),
      artifact = "raw", library_name = "kept", spectra = 4L
    )
  )
  report <- OpenSpecy:::.lib_library_retention_report(stages)

  expect_equal(report[library_name == "kept", status], "retained")
  expect_equal(report[library_name == "dropped", status], "dropped")
  expect_equal(
    report[library_name == "dropped", reason],
    "unresolved_other_policy"
  )
  expect_equal(report[library_name == "dropped", final_n], 0L)
})

test_that("confusion tables rank the largest misidentifications first", {
  tests <- data.table::data.table(
    algorithm = "logistic_regression", artifact = "derivative",
    model = "raman", source = "new",
    technique = "raman", provenance = "candidate",
    expected_class = c(rep("polyethylene", 10), rep("polypropylene", 4)),
    predicted_class = c(rep("polyethylene", 5), rep("polypropylene", 5),
                        rep("polypropylene", 3), "polyethylene")
  )
  confusion <- OpenSpecy:::.lib_confusion_table(tests)
  expect_named(confusion, c(
    "algorithm", "artifact", "model", "source", "technique", "provenance",
    "expected_class", "predicted_class", "spectra", "misidentified",
    "expected_class_spectra", "expected_class_fraction"
  ), ignore.order = TRUE)
  expect_true(confusion$misidentified[[1L]])
  expect_equal(confusion$spectra[[1L]], 5L)
  expect_equal(
    confusion[expected_class == "polyethylene", expected_class_fraction],
    c(0.5, 0.5)
  )
  expect_type(confusion$expected_class_fraction, "double")
  expect_named(OpenSpecy:::.lib_confusion_table(tests[0]), names(confusion))
})

test_that("model assessment compares error-mode accuracy percentages", {
  tests <- data.table::data.table(
    algorithm = "logistic_regression", artifact = "derivative",
    model = "raman", source = "new", technique = "raman",
    spectrum_id = paste0("s", 1:8),
    correct = c(TRUE, TRUE, TRUE, TRUE, FALSE, FALSE, FALSE, FALSE)
  )
  flags <- data.table::data.table(
    artifact = "derivative", technique = "raman",
    spectrum_id = paste0("s", 5:8), error_mode = "low_snr"
  )
  result <- OpenSpecy:::.lib_model_assessment_correlations(tests, flags)

  expect_named(
    result, names(OpenSpecy:::.lib_model_assessment_correlation_schema())
  )
  expect_identical(result$error_mode[[1L]], "low_snr")
  expect_equal(result$with_error_accuracy_pct, 0)
  expect_equal(result$without_error_accuracy_pct, 100)
  expect_equal(result$accuracy_difference_pct, -100)

  empty <- OpenSpecy:::.lib_model_assessment_correlations(tests[0], flags)
  expect_named(empty, names(result))
  expect_type(empty$with_error_accuracy_pct, "double")
})

test_that("reference metadata is ordered by missingness after all-NA removal", {
  x <- tiny_build_lib()
  x$metadata <- data.table::data.table(
    sample_name = colnames(x$spectra),
    complete = seq_len(ncol(x$spectra)),
    two_missing = c(NA, NA, rep("x", ncol(x$spectra) - 2L)),
    one_missing = c(NA, rep("y", ncol(x$spectra) - 1L)),
    entirely_missing = rep(NA_character_, ncol(x$spectra))
  )
  attr(x, "metadata_order_marker") <- "preserved"

  finalized <- OpenSpecy:::.lib_finalize_reference_metadata(
    libraries = list(raw = list(ftir = x)),
    medoids = list(derivative = list(ftir = x))
  )
  expected <- c("sample_name", "complete", "one_missing", "two_missing")
  expect_identical(names(finalized$libraries$raw$ftir$metadata), expected)
  expect_identical(names(finalized$medoids$derivative$ftir$metadata), expected)
  expect_true(check_OpenSpecy(finalized$libraries$raw$ftir))
  expect_identical(
    attr(finalized$libraries$raw$ftir, "metadata_order_marker"), "preserved"
  )
  expect_equal(finalized$assessment$dropped_columns, c(1L, 1L))
  expect_equal(finalized$assessment$dropped_names,
               rep("entirely_missing", 2L))
  expect_equal(finalized$assessment$maximum_missing, c(2L, 2L))
  expect_named(
    finalized$assessment,
    names(OpenSpecy:::.lib_metadata_finalization_schema())
  )
})

test_that("build_lib() preserves full source ranges through NA-aware recipes", {
  lib <- tiny_build_lib()
  left <- lib
  left$wavenumber <- lib$wavenumber[1:40]
  left$spectra <- lib$spectra[1:40, 1:4, drop = FALSE]
  left$metadata <- data.table::copy(lib$metadata[1:4])
  right <- lib
  right$wavenumber <- lib$wavenumber[22:61]
  right$spectra <- lib$spectra[22:61, 5:8, drop = FALSE]
  right$metadata <- data.table::copy(lib$metadata[5:8])

  built <- build_lib(list(left, right), dedupe = FALSE, signal_noise = FALSE)

  expect_true(all(diff(built$raw$wavenumber) == 6))
  expect_true(anyNA(built$raw$spectra))
  expect_true(anyNA(built$derivative$spectra))
  expect_true(any(is.finite(built$derivative$spectra[, 1])))
  expect_true(any(is.finite(built$nobaseline$spectra[, 8])))
})

test_that("streamed variable-axis paths match c_spec() interpolation", {
  lib <- tiny_build_lib()
  left <- filter_spec(lib, 1:4)
  left$wavenumber <- lib$wavenumber[1:40]
  left$spectra <- left$spectra[1:40, , drop = FALSE]
  right <- filter_spec(lib, 5:8)
  right$wavenumber <- lib$wavenumber[22:61]
  right$spectra <- right$spectra[22:61, , drop = FALSE]
  paths <- c(tempfile(fileext = ".rds"), tempfile(fileext = ".rds"))
  saveRDS(left, paths[[1L]])
  saveRDS(right, paths[[2L]])

  expected <- c_spec(list(left, right), range = "full", res = 6)
  actual <- build_lib(
    paths,
    recipes = list(raw = list()),
    range = "full",
    res = 6,
    dedupe = FALSE,
    convert_intensity = FALSE,
    signal_noise = FALSE,
    progress = FALSE
  )$raw

  expect_equal(actual$wavenumber, expected$wavenumber)
  expect_equal(actual$spectra, expected$spectra, tolerance = 1e-12)
  expect_equal(actual$metadata$sample_name, expected$metadata$sample_name)
  expect_true(check_OpenSpecy(actual))
})

test_that("source-stage hashes support spectra with no shared finite rows", {
  lib <- filter_spec(tiny_build_lib(), 1:2)
  midpoint <- floor(nrow(lib$spectra) / 2)
  lib$spectra[seq_len(midpoint), 1] <- NA_real_
  lib$spectra[seq.int(midpoint + 1L, nrow(lib$spectra)), 2] <- NA_real_

  built <- build_lib(
    lib, recipes = list(raw = list()), signal_noise = FALSE,
    progress = FALSE
  )$raw
  expect_true(check_OpenSpecy(built))
  expect_equal(ncol(built$spectra), 2L)
  expect_false(anyNA(built$metadata$sample_name))
  expect_identical(colnames(built$spectra), built$metadata$sample_name)
})

test_that("build_lib() applies baseline recipes across source-specific NA tails", {
  lib <- filter_spec(tiny_build_lib(), 1:2)
  lib$spectra[1:5, 1] <- NA_real_
  lib$spectra[57:61, 2] <- NA_real_

  built <- build_lib(
    list(lib),
    recipes = list(nobaseline = list(
      conform_spec = FALSE,
      smooth_intens = FALSE,
      subtr_baseline = TRUE,
      make_rel = TRUE
    )),
    dedupe = FALSE,
    convert_intensity = FALSE,
    signal_noise = FALSE
  )$nobaseline
  expected <- manage_na(lib, fun = subtr_baseline)

  expect_equal(built$spectra, expected$spectra, tolerance = 1e-12)
  expect_equal(attr(built, "baseline"), "nobaseline")
})

test_that("extdata files combine into a mini library", {
  mini_files <- c(
    read_extdata("raman_hdpe.csv"),
    read_extdata("ftir_ldpe_soil.asp"),
    read_extdata("raman_atacamit.spc")
  )

  mini <- read_any(mini_files, c_spec_args = list(range = "common", res = 10))
  expect_true(check_OpenSpecy(mini))
  expect_equal(ncol(mini$spectra), 3)

  lookup <- data.table::data.table(
    file_name = basename(mini_files),
    material = c("hdpe", "ldpe in soil", "atacamite"),
    material_type = c("plastic", "plastic", "mineral")
  )
  mini <- join_lib_metadata(mini, lookup, by = "file_name",
                            require_complete = TRUE)
  built <- build_lib(
    mini_files,
    recipes = list(raw = list()),
    metadata_lookups = lookup,
    dedupe = FALSE,
    convert_intensity = FALSE,
    signal_noise = FALSE
  )
  expect_true(check_OpenSpecy(built$raw))
  expect_true(all(c("material", "material_type") %in% names(built$raw$metadata)))
})

test_that("build_lib() discovers helper data and reuses one artifact bundle", {
  # This exercises the complete multi-artifact reference-library workflow.
  skip_on_cran()

  lib <- tiny_build_lib()
  lib$spectra[30, ] <- lib$spectra[30, ] + 10
  lib$metadata[, `:=`(
    spectrum_identity = label,
    organization = source,
    user_name = source,
    spectrum_id = sample_name
  )]
  spectra <- do.call(cbind, lapply(seq_len(10), function(copy) {
    out <- lib$spectra + copy / 1000
    colnames(out) <- paste0(colnames(out), "_", copy)
    out
  }))
  metadata <- data.table::rbindlist(lapply(seq_len(10), function(copy) {
    out <- data.table::copy(lib$metadata)
    out[, `:=`(
      sample_name = paste0(sample_name, "_", copy),
      spectrum_id = paste0(spectrum_id, "_", copy)
    )]
    out
  }))
  lib <- as_OpenSpecy(
    lib$wavenumber, spectra = spectra, metadata = metadata,
    attributes = list(intensity_unit = "absorbance")
  )
  workflow_root <- file.path(
    tempdir(), paste0("workflow-", sample.int(1e8, 1))
  )
  workflow_data <- file.path(workflow_root, "data")
  output_dir <- file.path(tempdir(), paste0("output-", sample.int(1e8, 1)))
  dir.create(workflow_data, recursive = TRUE)
  old_working_dir <- setwd(workflow_root)
  on.exit(setwd(old_working_dir), add = TRUE)
  data.table::fwrite(data.table::data.table(
    spectrum_identity = unique(lib$metadata$spectrum_identity),
    material = paste0("material_", seq_along(unique(
      lib$metadata$spectrum_identity
    )))
  ), file.path(workflow_data, "classes_reference.csv"))
  data.table::fwrite(data.table::data.table(
    pattern = "^never[0-9]+$", material = "other material"
  ), file.path(workflow_data, "classes_regex.csv"))
  data.table::fwrite(data.table::data.table(
    organization = c("a", "b", "c"), library_type = "test",
    spectrum_type = "ftir"
  ), file.path(workflow_data, "library_types.csv"))
  classes <- data.table::fread(
    file.path(workflow_data, "classes_reference.csv")
  )
  data.table::fwrite(data.table::data.table(
    material = classes$material,
    material_class = rep(c("polyclass_a", "polyclass_b"),
                         length.out = nrow(classes)),
    material_type = "plastic"
  ), file.path(workflow_data, "material_hierarchy.csv"))
  data.table::fwrite(data.table::data.table(sample_name = character()),
                     file.path(workflow_data, "known_bad_ids.csv"))
  data.table::fwrite(data.table::data.table(
    metadata_column = "unused_legacy_column"
  ), file.path(workflow_data, "metadata_drop_columns.csv"))
  data.table::fwrite(data.table::data.table(
    pattern = "(^|[^[:alnum:]])paint([^[:alnum:]]|$)",
    material_form = "paint"
  ), file.path(workflow_data, "material_form_regex.csv"))
  use_classes <- sort(unique(data.table::fread(
    file.path(workflow_data, "material_hierarchy.csv")
  )$material_class))
  data.table::fwrite(data.table::data.table(
    material_class = use_classes,
    common_use = rep(NA_character_, length(use_classes)),
    consumer_mass_share = rep(NA_real_, length(use_classes)),
    industrial_mass_share = rep(NA_real_, length(use_classes)),
    unclassified_mass_share = rep(NA_real_, length(use_classes)),
    basis_year = rep(NA_integer_, length(use_classes)),
    geography = rep(NA_character_, length(use_classes)),
    source_url = rep(NA_character_, length(use_classes)),
    retrieved_date = rep(NA_character_, length(use_classes)),
    evidence_notes = rep(NA_character_, length(use_classes))
  ), file.path(workflow_data, "common_use_reference.csv"))
  fixture_prune <- list(
    derivative = list(min_n = 10, progress = FALSE),
    nobaseline = list(min_n = 10, progress = FALSE)
  )

  first <- suppressWarnings(build_lib(
    lib, output_dir = output_dir,
    previous_library_dir = NULL, dedupe = FALSE, signal_noise = FALSE,
    progress = FALSE,
    prune = fixture_prune,
    recipes = list(raw = list(), derivative = list(), nobaseline = list())
  ))
  expect_named(first, c("libraries", "medoids", "models", "assessments"))
  expect_named(first$libraries, c("raw", "derivative", "nobaseline"))
  expect_named(first$medoids, c("derivative", "nobaseline"))
  expect_named(first$models, "logistic_regression")
  expect_true(all(vapply(first$libraries, function(recipe) {
    all(vapply(recipe, check_OpenSpecy, logical(1)))
  }, logical(1))))
  expect_true(all(vapply(first$medoids, function(recipe) {
    all(vapply(recipe, check_OpenSpecy, logical(1)))
  }, logical(1))))
  expect_named(first$assessments,
               c("cleanup", "ref_lib", "medoid", "model", "functionality"))
  expect_lte(sum(lengths(first$assessments)), 10L)
  expect_true(all(lengths(first$assessments) > 0L))
  expect_true(all(vapply(
    unlist(first$assessments, recursive = FALSE), nrow, integer(1L)
  ) > 0L))
  expect_identical(attr(first$assessments, "assessment_schema_version"),
                   "2.0.0")
  expect_named(first$assessments$cleanup$dropped_spectrum_identities,
               "spectrum_identity")
  retention <- first$assessments$cleanup$summary[
    assessment_kind == "library_retention"
  ]
  expect_gt(nrow(retention), 0L)
  expect_true(all(c("library_name", "prepared_n", "final_n", "status",
                    "reason") %in% names(retention)))
  expect_true(all(vapply(first$libraries$raw, function(object) {
    all(c("library_name", "material_form", "common_use") %in%
          names(object$metadata))
  }, logical(1))))
  expect_true(all(vapply(first$libraries$raw, function(object) {
    is.list(attr(object, "material_form_enrichment_report", exact = TRUE)) &&
      is.list(attr(object, "common_use_enrichment_report", exact = TRUE))
  }, logical(1))))
  upstream <- attr(first$assessments, "upstream_assessments", exact = TRUE)
  expect_true(all(c(
    "material_form_enrichment", "material_form_matches",
    "material_form_clashes", "common_use_enrichment", "common_use_coverage",
    "pruning_removals"
  ) %in% names(upstream)))
  release_dir <- attr(first, "output_dir")
  expect_true(all(file.exists(file.path(
    release_dir,
    c("raw.rds", "derivative.rds", "nobaseline.rds",
      "medoid_derivative.rds", "medoid_nobaseline.rds",
      "model_derivative.rds", "model_nobaseline.rds",
      "model_logistic_regression_derivative.rds",
      "model_logistic_regression_nobaseline.rds",
      "assessments.rds", "quarantined_spectra.rds",
      "reference_library_build.rds")
  ))))
  quarantine <- readRDS(file.path(release_dir, "quarantined_spectra.rds"))
  expect_s3_class(quarantine, "OpenSpecyQuarantine")
  expect_identical(quarantine$manifest$schema,
                   "OpenSpecy_quarantined_spectra_v1")
  release_index <- readRDS(file.path(release_dir, "reference_library_build.rds"))
  expect_identical(
    release_index$schema, "OpenSpecy_reference_build_index_v1"
  )
  expect_false(any(c("libraries", "medoids", "models") %in%
                     names(release_index)))
  expect_lt(file.info(file.path(
    release_dir, "reference_library_build.rds"
  ))$size, 1e6)

  release_assessments <- readRDS(file.path(release_dir, "assessments.rds"))
  expect_identical(
    attr(release_assessments, "assessment_schema_version"), "2.0.0"
  )
  expect_false(any(c("scope", "expected_class") %in%
                     names(release_assessments$ref_lib$accuracy)))
  expect_true("model_training_tests" %in%
                names(attr(release_assessments, "evidence")))

  report_attributes <- c(
    "join_report", "metadata_lookup_reports",
    "spectrum_identity_cleanup_report", "build_stage_report",
    "dropped_spectrum_identities", "class_prediction_report",
    "class_coverage_report", "other_review_report", "other_filter_report",
    "quality_control_report", "prune_report", "identification_support",
    "identification_dropped_ids", "identification_minimum_observed",
    "identification_finite_coverage", "range_flat_drops",
    "library_retention_stages"
  )
  for (component in c("raw", "derivative", "nobaseline",
                      "medoid_derivative", "medoid_nobaseline")) {
    artifact <- readRDS(file.path(release_dir, paste0(component, ".rds")))
    for (object in artifact) {
      expect_true(check_OpenSpecy(object))
      expect_length(intersect(names(attributes(object)), report_attributes), 0L)
      expect_null(attr(object$metadata, "join_report", exact = TRUE))
    }
  }
  model_fields <- OpenSpecy:::.lib_runtime_model_fields()
  for (component in c(
    "model_logistic_regression_derivative",
    "model_logistic_regression_nobaseline"
  )) {
    artifact <- readRDS(file.path(release_dir, paste0(component, ".rds")))
    for (model in artifact) {
      expect_true(all(names(model) %in% model_fields))
      expect_false(any(c(
        "tests", "lambda_metrics", "oob_metrics", "oob_class_accuracy",
        "feature_importance", "training_parameters", "class_weights",
        "support", "class_support"
      ) %in% names(model)))
      expect_null(attr(model, "training_warnings", exact = TRUE))
    }
  }

  resolved <- OpenSpecy:::.lib_resolve_rebuild_input(
    file.path(release_dir, "reference_library_build.rds")
  )
  expect_named(resolved$libraries, c("raw", "derivative", "nobaseline"))
  expect_named(resolved$medoids, c("derivative", "nobaseline"))
  expect_named(resolved$models, "logistic_regression")
  expect_s3_class(resolved$quarantine, "OpenSpecyQuarantine")
  expect_identical(
    resolved$quarantine$manifest$schema,
    "OpenSpecy_quarantined_spectra_v1"
  )

  second <- suppressWarnings(build_lib(
    lib, output_dir = output_dir,
    previous_library_dir = NULL, dedupe = FALSE, signal_noise = FALSE,
    progress = FALSE, reuse = TRUE,
    prune = fixture_prune,
    recipes = list(raw = list(), derivative = list(), nobaseline = list())
  ))
  second_evidence <- attr(second$assessments, "evidence")
  expect_true(all(
    second_evidence$output_manifest$status == "available"
  ))
  expect_equal(second$libraries$raw$ftir$spectra,
               first$libraries$raw$ftir$spectra)

  rebuilt <- suppressWarnings(build_lib(
    lib, output_dir = output_dir,
    previous_library_dir = NULL, dedupe = FALSE, signal_noise = FALSE,
    progress = FALSE, reuse = FALSE,
    prune = fixture_prune,
    recipes = list(raw = list(), derivative = list(), nobaseline = list())
  ))
  expect_named(rebuilt$assessments,
               c("cleanup", "ref_lib", "medoid", "model", "functionality"))
})

test_that("source-local reference splits prevent self-match leakage", {
  old <- tiny_build_lib()
  new <- old
  new$metadata$sample_name_old <- new$metadata$sample_name
  new$metadata$sample_name <- paste0("rebuilt_", new$metadata$sample_name)
  colnames(new$spectra) <- new$metadata$sample_name
  split <- OpenSpecy:::.lib_source_split(
    new, artifact = "raw", source = "new", seed = 71, holdout = 0.25
  )
  expect_identical(anyDuplicated(split$manifest$group_id), 0L)
  expect_true(all(split$manifest$source == "new"))
  expect_true(all(split$manifest$split %in% c("train", "test")))
  expect_equal(
    sum(split$manifest$split == "test"),
    round(nrow(split$manifest) * 0.25)
  )
  expect_length(intersect(
    split$manifest[split == "train", group_id],
    split$manifest[split == "test", group_id]
  ), 0L)

  messages <- capture.output(
    tests <- OpenSpecy:::.lib_reference_holdout_test(
      reference = new, data = new, split = split,
      artifact = "raw", source = "new", progress = TRUE
    ),
    type = "message"
  )
  expect_match(paste(messages, collapse = "\n"), "full correlation complete")
  expect_true(all(tests$split == "test"))
  expect_true(all(tests$provenance == "source_local_reference_holdout"))

  old_split <- OpenSpecy:::.lib_source_split(
    old, artifact = "raw", source = "old", seed = 72, holdout = 0.25
  )
  old_tests <- OpenSpecy:::.lib_reference_holdout_test(
    reference = old, data = old, split = old_split,
    artifact = "raw", source = "old", progress = FALSE
  )
  expect_true(all(old_split$manifest$source == "old"))
  expect_true(all(old_tests$source == "old"))
})

test_that("medoid assessment searches the complete corresponding library", {
  lib <- tiny_build_lib()
  medoid <- filter_spec(lib, c(1L, 3L))
  messages <- capture.output(
    tests <- OpenSpecy:::.lib_reference_complete_test(
      medoid, lib, artifact = "medoid_derivative_ftir", source = "new",
      progress = TRUE, block_size = 2L
    ),
    type = "message"
  )
  expect_match(paste(messages, collapse = "\n"),
               "medoid identifying complete dataset")
  expect_equal(nrow(tests), ncol(lib$spectra))
  expect_setequal(tests$spectrum_id, lib$metadata$sample_name)
  expect_true(all(tests$split == "complete"))
  expect_true(all(tests$provenance == "complete_medoid_reference_search"))
})

test_that("complete old-new assessments directly test deployed models", {
  skip_if_not_installed("glmnet")
  skip_if_not_installed("ranger")
  small <- tiny_build_lib()
  spectra <- do.call(cbind, lapply(seq_len(5), function(i) {
    small$spectra + i / 1000
  }))
  metadata <- data.table::rbindlist(lapply(seq_len(5), function(i) {
    out <- data.table::copy(small$metadata)
    out$sample_name <- paste0(out$sample_name, "_", i)
    out
  }))
  colnames(spectra) <- metadata$sample_name
  lib <- as_OpenSpecy(
    small$wavenumber, spectra = spectra, metadata = metadata,
    attributes = list(intensity_unit = "absorbance")
  )
  model_input <- restrict_range(
    lib, min = 800, max = 3200, make_rel = FALSE
  )
  model <- suppressWarnings(build_model_lib(model_input))
  rf_model <- train_spec_model(
    model_input, method = "random_forest", num.trees = 20L,
    min.node.size = 1L, mtry = 4L
  )
  model_set <- list(both = model, ftir = model, raman = NULL)
  build <- list(
    libraries = list(
      raw = list(ftir = lib), derivative = list(ftir = lib),
      nobaseline = list(ftir = lib)
    ),
    medoids = list(
      derivative = list(ftir = model_input),
      nobaseline = list(ftir = model_input)
    ),
    models = list(
      logistic_regression = list(
        derivative = model_set, nobaseline = model_set
      ),
      random_forest = list(raw = list(ftir = rf_model))
    ),
    assessments = list()
  )
  previous <- file.path(tempdir(), paste0("previous-", sample.int(1e8, 1)))
  dir.create(previous, recursive = TRUE)
  typed_lib <- list(ftir = lib)
  typed_medoid <- list(ftir = model_input)
  saveRDS(typed_lib, file.path(previous, "raw.rds"))
  saveRDS(typed_lib, file.path(previous, "derivative.rds"))
  saveRDS(typed_lib, file.path(previous, "nobaseline.rds"))
  saveRDS(typed_medoid, file.path(previous, "medoid_derivative.rds"))
  saveRDS(typed_medoid, file.path(previous, "medoid_nobaseline.rds"))
  saveRDS(model_set, file.path(previous, "model_derivative.rds"))
  saveRDS(model_set, file.path(previous, "model_nobaseline.rds"))

  local_mocked_bindings(
    train_spec_model = function(...) {
      stop("assessment must not retrain a model", call. = FALSE)
    },
    .package = "OpenSpecy"
  )

  comparison <- suppressWarnings(OpenSpecy:::.lib_compare_reference_build(
    build, previous_library_dir = previous,
    seed = 211, holdout = 0.25, progress = FALSE
  ))
  expect_true(all(c(
    "models", "split_manifest", "library_identification",
    "model_identification", "model_assessment_correlations",
    "assess_spec_shifts", "old_new_compatibility"
  ) %in% names(comparison)))
  expect_equal(unique(comparison$split_manifest$artifact), c(
    "raw_ftir", "derivative_ftir", "nobaseline_ftir"
  ))
  expect_setequal(unique(comparison$split_manifest$source), c("new", "old"))
  expect_gt(nrow(comparison$library_identification), 0L)
  expect_gt(nrow(comparison$model_identification), 0L)
  expect_setequal(
    unique(comparison$model_identification$algorithm),
    c("logistic_regression", "random_forest")
  )
  expect_equal(nrow(comparison$model_assessment_correlations), 0L)
  expect_gt(nrow(comparison$assess_spec_shifts), 0L)
  expect_equal(
    comparison$library_tests[
      artifact == "medoid_derivative_ftir" & source == "new", .N
    ],
    ncol(lib$spectra)
  )
  expect_true(all(comparison$library_tests[
    grepl("^medoid_", artifact), split
  ] == "complete"))
  expect_true(all(
    comparison$models$logistic_regression$derivative$ftir$tests$provenance ==
      "new_logistic_regression_complete_dataset"
  ))
  expect_equal(
    nrow(comparison$models$logistic_regression$derivative$ftir$tests),
    ncol(lib$spectra)
  )
  expect_true(all(
    comparison$models$random_forest$raw$ftir$tests$provenance ==
      "new_random_forest_complete_dataset"
  ))
  expect_equal(
    comparison$models$random_forest$raw$ftir$observation_count,
    ncol(model_input$spectra)
  )
})

test_that("assessment review is process nested, compact, and nonsparse", {
  identification <- data.table::data.table(
    artifact = rep(c("raw_ftir", "medoid_derivative_ftir"), each = 2L),
    source = rep(c("old", "new"), 2L), technique = "ftir",
    provenance = "grouped", macro_class_accuracy = c(.5, .8, .9, .7),
    coverage = 1, overall_accuracy = c(.6, .85, .91, .75),
    spectra = 20L, evaluated = 20L, classes = 2L,
    evaluated_classes = 2L, mean_score = .8
  )
  confusion <- data.table::data.table(
    artifact = rep("raw_ftir", 4L), source = rep(c("old", "new"), 2L),
    technique = "ftir", provenance = "grouped",
    expected_class = c("a", "a", "b", "b"),
    predicted_class = c("b", "b", "a", "a"), misidentified = TRUE,
    spectra = c(2L, 7L, 1L, 4L), expected_class_spectra = 10L,
    expected_class_fraction = c(.2, .7, .1, .4)
  )
  model_identification <- data.table::copy(identification[artifact == "raw_ftir"])
  model_identification[, `:=`(
    algorithm = "logistic_regression", artifact = "derivative",
    model = "ftir"
  )]
  correlations <- data.table::data.table(
    algorithm = "logistic_regression", artifact = "derivative",
    model = "ftir", technique = "ftir", error_mode = "low_snr",
    with_error_accuracy_pct = 50,
    without_error_accuracy_pct = 90,
    accuracy_difference_pct = -40
  )
  reviewed <- OpenSpecy:::.lib_assessment_review(list(
    build_summary = data.table::data.table(artifact = "raw_ftir", spectra = 20L),
    dropped_spectrum_identities = data.table::data.table(
      spectrum_identity = c("z", "a", "z")
    ),
    library_identification = identification,
    library_class_accuracy = data.table::data.table(
      artifact = "raw_ftir", source = "new", technique = "ftir",
      provenance = "grouped", expected_class = "class_a",
      spectra = 10L, evaluated = 10L, coverage = 1,
      class_accuracy = 0.5
    ),
    library_confusion = confusion,
    model_identification = model_identification,
    model_class_accuracy = data.table::data.table(
      algorithm = "logistic_regression", artifact = "derivative",
      model = "ftir", source = "new", technique = "ftir",
      provenance = "grouped", expected_class = "class_a",
      spectra = 10L, evaluated = 10L, coverage = 1,
      class_accuracy = 0.5
    ),
    model_confusion = data.table::copy(confusion)[, `:=`(
      algorithm = "logistic_regression", artifact = "derivative", model = "ftir"
    )],
    model_assessment_correlations = correlations,
    assess_spec_shifts = data.table::data.table(
      artifact = "raw_ftir", check = "signal", rate_old = .2,
      rate_new = .1, rate_shift = -.1
    ),
    old_new_compatibility = data.table::data.table(
      artifact = "raw_ftir", spectra_old = 10L, spectra_new = 20L,
      spectra_shift = 10L
    ),
    output_manifest = data.table::data.table(
      component = "raw", status = "available", path = "raw.rds", size = 1
    )
  ))

  expect_named(reviewed,
               c("cleanup", "ref_lib", "medoid", "model", "functionality"))
  expect_lte(sum(lengths(reviewed)), 10L)
  expect_true(all(lengths(reviewed) > 0L))
  expect_identical(
    reviewed$cleanup$dropped_spectrum_identities$spectrum_identity,
    c("a", "z")
  )
  accuracy_names <- names(reviewed$ref_lib$accuracy)
  expect_false(any(c("scope", "expected_class", "class_accuracy") %in%
                     accuracy_names))
  expect_true(all(c("source", "overall_accuracy_pct",
                    "macro_class_accuracy_pct", "class_count") %in%
                    accuracy_names))
  expect_false(any(c("coverage_pct", "mean_score",
                     "evaluated_classes") %in% accuracy_names))
  expect_identical(reviewed$ref_lib$accuracy$source, c("old", "new"))
  expect_equal(reviewed$ref_lib$accuracy$macro_class_accuracy_pct, c(50, 80))
  expect_identical(reviewed$ref_lib$accuracy$class_count, c(2L, 2L))
  expect_identical(
    reviewed$model$error_mode_accuracy$accuracy_difference_pct, -40
  )
  sparse <- unlist(lapply(
    unlist(reviewed, recursive = FALSE),
    function(value) vapply(value, function(column) mean(is.na(column)),
                           numeric(1L))
  ))
  expect_true(all(sparse <= 0.10))
})

test_that("assessment shifts merge findings, omit passes, and rank rate shifts", {
  summary <- data.table::data.table(
    artifact = rep("raw_ftir", 6L), source = rep(c("old", "new"), each = 3L),
    check = rep(c("signal", "signal", "range"), 2L),
    status = rep(c("pass", "warning", "error"), 2L),
    count = c(90L, 5L, 1L, 80L, 10L, 4L),
    finding_count = c(0L, 5L, 1L, 0L, 10L, 4L),
    example_ids = c("", "a", "b", "", "c", "d"), spectra = 100L
  )
  shifts <- OpenSpecy:::.lib_assessment_shift_table(summary)
  expect_false("status" %in% names(shifts))
  expect_equal(shifts[check == "signal", count_old], 5L)
  expect_equal(shifts[check == "signal", count_new], 10L)
  expect_true(all(diff(shifts$rate_shift) <= 0))
})

test_that("stable groups connect physical IDs and exact spectral content", {
  x <- tiny_build_lib()
  x$metadata$sample_name_old <- paste0("legacy_", seq_len(nrow(x$metadata)))
  x$metadata$sample_name_old[2] <- x$metadata$sample_name_old[1]
  x$spectra[, 4] <- x$spectra[, 3]
  groups <- OpenSpecy:::.lib_stable_group_info(x)
  expect_equal(groups$group_id[1], groups$group_id[2])
  expect_equal(groups$group_id[3], groups$group_id[4])
  split <- OpenSpecy:::.lib_source_split(
    x, artifact = "raw", source = "new", seed = 1, holdout = .25
  )
  expect_identical(
    split$manifest[, data.table::uniqueN(split), by = group_id][, max(V1)],
    1L
  )
})

test_that("immutable promotion rejects a changed payload", {
  path <- tempfile(fileext = ".rds")
  first <- OpenSpecy:::.lib_promote_rds(list(value = 1L), path)
  expect_equal(first$status, "promoted")
  second <- OpenSpecy:::.lib_promote_rds(list(value = 1L), path)
  expect_equal(second$status, "verified_existing")

  semantic_path <- tempfile(fileext = ".rds")
  saveRDS(list(value = 1L), semantic_path, compress = FALSE)
  semantic_checksum <- OpenSpecy:::.lib_sha256_file(semantic_path)
  semantic <- OpenSpecy:::.lib_promote_rds(
    list(value = 1L), semantic_path
  )
  expect_equal(semantic$status, "verified_existing")
  expect_identical(
    OpenSpecy:::.lib_sha256_file(semantic_path), semantic_checksum
  )
  expect_error(
    OpenSpecy:::.lib_promote_rds(list(value = 2L), path),
    "Immutable release artifact differs"
  )
})

test_that("completed release manifests authorize immutable byte reuse", {
  path <- tempfile(fileext = ".rds")
  manifest <- OpenSpecy:::.lib_promote_rds(list(value = 1L), path)
  attr(manifest, "build_signature") <- "signed-build"

  verified <- OpenSpecy:::.lib_verify_manifested_release_artifact(
    path, manifest
  )
  expect_equal(verified$status, "verified_existing")
  expect_identical(verified$checksum, manifest$checksum)

  saveRDS(list(value = 2L), path)
  expect_null(OpenSpecy:::.lib_verify_manifested_release_artifact(
    path, manifest
  ))
})

test_that("file signature caching preserves hashes and invalidates on metadata", {
  path <- tempfile(fileext = ".txt")
  writeLines("first", path)
  first <- OpenSpecy:::.lib_file_signatures(path)
  second <- OpenSpecy:::.lib_file_signatures(path)
  expect_identical(first$checksum, second$checksum)
  writeLines("a different sized payload", path)
  changed <- OpenSpecy:::.lib_file_signatures(path)
  expect_false(identical(first$checksum, changed$checksum))
})

test_that("support and fill helpers use the intended axes", {
  x <- tiny_build_lib()
  x$spectra[,] <- NA_real_
  x$spectra[seq_len(6), 1] <- seq_len(6)
  x$spectra[seq_len(7), 2] <- seq_len(7)
  support <- OpenSpecy:::.lib_filter_spectral_support(x, min_fraction = 0.1)
  expect_equal(support$audit$minimum_observed, rep(7L, 8))
  expect_false(support$audit$retained[1])
  expect_true(support$audit$retained[2])

  spectra <- matrix(c(1, NA, 3, 10, 20, NA), nrow = 3)
  spectrum_fill <- OpenSpecy:::.lib_spectrum_mean_replace(spectra)
  expect_equal(spectrum_fill[, 1], c(1, 2, 3))
  expect_equal(spectrum_fill[, 2], c(10, 20, 15))
  wave_fill <- OpenSpecy:::.lib_wavenumber_mean_replace(spectra)
  expect_equal(wave_fill$means, c(5.5, 20, 3))
  expect_equal(wave_fill$spectra[, 1], c(1, 20, 3))
  expect_equal(wave_fill$spectra[, 2], c(10, 20, 3))
})

test_that("medoid selection restores original missing values", {
  x <- tiny_build_lib()
  x$spectra[10:20, 1] <- NA_real_
  ids <- reduce_lib(
    x, group_cols = "material_class", k = 2, min_n = 2,
    return = "ids", progress = FALSE
  )
  restored <- filter_spec(x, ids)
  if ("s1" %in% ids) {
    expect_true(anyNA(restored$spectra[, match("s1", ids)]))
  }
  flat <- x
  flat$spectra[, 1] <- NA_real_
  flat$spectra[seq_len(7), 1] <- 1
  expect_no_error(reduce_lib(
    flat, group_cols = "material_class", k = 2, min_n = 2,
    return = "ids", progress = FALSE
  ))
})

test_that("macro lambda metrics choose class-balanced accuracy", {
  outcome <- c(1L, 1L, 1L, 2L)
  predictions <- array(0, dim = c(4, 2, 2))
  predictions[cbind(seq_len(4), c(1, 1, 1, 1), 1)] <- 1
  predictions[cbind(seq_len(4), c(1, 2, 2, 2), 2)] <- 1
  metrics <- OpenSpecy:::.lib_macro_lambda_metrics(
    predictions, outcome, lambda = c(1, 0.1)
  )
  expect_equal(metrics$macro_class_accuracy, c(0.5, 2 / 3))
  expect_equal(metrics$overall_accuracy, c(0.75, 0.5))
  expect_identical(which(metrics$selected), 2L)
})

test_that("rebuild_lib_artifacts reuses completed libraries downstream", {
  skip_if_not_installed("glmnet")
  base <- tiny_build_lib()
  libraries <- lapply(seq_len(3), function(i) {
    spectra <- do.call(cbind, lapply(seq_len(3), function(copy) {
      base$spectra + copy / 1000
    }))
    metadata <- data.table::rbindlist(lapply(seq_len(3), function(copy) {
      out <- data.table::copy(base$metadata)
      out$sample_name <- paste0(out$sample_name, "_", copy)
      out
    }))
    colnames(spectra) <- metadata$sample_name
    as_OpenSpecy(base$wavenumber, spectra = spectra, metadata = metadata)
  })
  names(libraries) <- c("raw", "derivative", "nobaseline")
  libraries <- lapply(libraries, function(x) list(ftir = x))
  libraries$derivative$ftir$spectra[10:20, 1] <- NA_real_
  input <- tempfile(fileext = ".rds")
  saveRDS(list(libraries = libraries, assessments = list()), input)
  output <- tempfile("downstream-build-")

  first <- suppressWarnings(rebuild_lib_artifacts(
    input, output_dir = output, previous_library_dir = NULL,
    reuse = TRUE, progress = FALSE
  ))
  expect_named(first, c("libraries", "medoids", "models", "assessments"))
  expect_true(anyNA(first$medoids$derivative$ftir$spectra))
  expect_true(
    first$models$logistic_regression$derivative$ftir$fill_replaced > 0L
  )
  expect_named(first$assessments,
               c("cleanup", "ref_lib", "medoid", "model", "functionality"))
  second <- suppressWarnings(rebuild_lib_artifacts(
    input, output_dir = output, previous_library_dir = NULL,
    reuse = TRUE, progress = FALSE
  ))
  second_evidence <- attr(second$assessments, "evidence")
  expect_true(all(
    second_evidence$output_manifest$status == "available"
  ))
})

test_that("reference quality schemas and type ranges are stable", {
  expect_named(OpenSpecy:::.lib_warning_schema(),
               c("algorithm", "artifact", "model", "warning"))
  expect_type(OpenSpecy:::.lib_warning_schema()$warning, "character")

  x <- tiny_build_lib()
  x$metadata$spectrum_type <- rep(c("ftir", "raman"), each = 4)
  typed <- OpenSpecy:::.lib_partition_reference_libraries(
    list(raw = x), report = NULL
  )$raw
  expect_equal(range(typed$ftir$wavenumber), c(400, 4000))
  expect_equal(range(typed$raman$wavenumber), c(200, 4000))
})

test_that("range restriction removes spectra that become flat", {
  x <- tiny_build_lib()
  x$metadata$spectrum_type <- "raman"
  x$spectra[, 1] <- 1
  outside_partition <- x$wavenumber > 4000
  x$spectra[outside_partition, 1] <- seq_len(sum(outside_partition))

  typed <- OpenSpecy:::.lib_partition_reference_libraries(
    list(raw = x), report = NULL
  )$raw$raman
  expect_false("s1" %in% typed$metadata$sample_name)
  partition_drops <- attr(typed, "range_flat_drops", exact = TRUE)
  expect_equal(partition_drops$spectrum_id, "s1")
  expect_equal(partition_drops$reason, "post_partition_flat_spectrum")

  model_source <- tiny_build_lib()
  model_source$metadata$spectrum_type <- "raman"
  model_source$spectra[, 1] <- 1
  outside_model <- model_source$wavenumber > 3200
  model_source$spectra[outside_model, 1] <- seq_len(sum(outside_model))
  medoids <- OpenSpecy:::.lib_build_medoids(
    list(derivative = list(raman = model_source)),
    report = function(...) NULL, progress = FALSE
  )$derivative$raman
  expect_false("s1" %in% medoids$metadata$sample_name)
  model_drops <- attr(medoids, "range_flat_drops", exact = TRUE)
  expect_equal(model_drops$spectrum_id, "s1")
  expect_equal(model_drops$reason, "post_model_range_flat_spectrum")
})

test_that("identification ranges retain partial spectra and drop empty ones", {
  axis <- seq(400, 4000, by = 6)
  spectra <- cbind(
    complete = sin(axis / 100),
    partial = ifelse(axis < 1200, NA_real_, cos(axis / 90)),
    outside_only = ifelse(axis < 800, 1, NA_real_)
  )
  object <- as_OpenSpecy(
    axis, spectra,
    metadata = data.table::data.table(
      sample_name = colnames(spectra),
      spectrum_type = "ftir",
      material_class = "plastic"
    )
  )

  restricted <- OpenSpecy:::.lib_restrict_model_range(object, "ftir")
  expect_equal(range(restricted$wavenumber), c(802, 3196))
  expect_identical(colnames(restricted$spectra), c("complete", "partial"))
  expect_identical(
    attr(restricted, "identification_dropped_ids"), "outside_only"
  )
  expect_true(anyNA(restricted$spectra[, "partial"]))
})

test_that("reference regex table contains only genuinely variable rules", {
  expect_true(OpenSpecy:::.lib_regex_is_exact_literal(
    "^poly\\(amide\\)$"
  ))
  expect_false(OpenSpecy:::.lib_regex_is_exact_literal(
    "^olefin[[:space:]]*\\(pe(?:[[:space:]]|\\x2c|\\)|$)"
  ))
  expect_false(OpenSpecy:::.lib_regex_is_exact_literal(
    "^(?:cotton|wool|silk)$"
  ))
  regex_reference <- data.table::fread(
    reference_workflow_data_path("classes_regex.csv")
  )
  expect_false(any(vapply(
    regex_reference$pattern,
    OpenSpecy:::.lib_regex_is_exact_literal,
    logical(1)
  )))
  exact <- data.table::fread(
    reference_workflow_data_path("classes_reference.csv")
  )
  expect_true(all(c(
    "epoxide", "poly 1-butene isotactic", "poly 4-methyl-1-pentene",
    "poly(amide)", "poly(styrene)", "poly(vinylchloride)",
    "polyethylene glycol"
  ) %in% exact$spectrum_identity))
  predicted <- predict_class_reference(
    data.table::data.table(
      spectrum_identity = c(
        "sample sbr rubber", "sample epdm gasket", "sample acrylic paint",
        "sample urethane paint", "sample alkyd varnish"
      ),
      material = NA_character_
    ),
    regex_reference, return = "table"
  )
  expect_identical(
    predicted$material,
    c(
      "styrene-butadiene",
      "epdm rubber (ethylene propylene diene monomer rubber)",
      "polyacrylates", "polyurethanes", "polyesters"
    )
  )
})

test_that("official material hierarchy uses concise reviewed polymer classes", {
  hierarchy <- data.table::fread(
    reference_workflow_data_path("material_hierarchy.csv")
  )
  expect_identical(anyDuplicated(hierarchy$material), 0L)
  expect_false(anyNA(hierarchy$material_class))
  expect_false(any(!nzchar(trimws(hierarchy$material_class))))
  expect_identical(
    hierarchy[material == "poly(ethylene)", material_class], "polyethylene"
  )
  expect_identical(
    hierarchy[material == "poly(propylene)", material_class], "polypropylene"
  )
  expect_identical(
    hierarchy[material == "poly(tetrafluoroethylene)", material_class],
    "polytetrafluoroethylene"
  )
  expect_identical(
    hierarchy[material == "poly(ethylene-vinyl acetate) (EVA)",
              material_class],
    "polyethylene-vinyl acetate"
  )
  expect_identical(
    hierarchy[material == "styrene-butadiene", material_class],
    "poly(styrene-butadiene) rubber"
  )
  expect_identical(
    hierarchy[
      material == "epdm rubber (ethylene propylene diene monomer rubber)",
      material_class
    ],
    "poly(ethylene-propylene-diene) rubber"
  )
  expect_identical(
    hierarchy[material == "poly(ethylene glycol)", material_class],
    "polyethers"
  )
  parenthetical_classes <- unique(
    hierarchy[grepl("[()]", material_class), material_class]
  )
  expect_setequal(parenthetical_classes, c(
    "poly(ethylene-propylene-diene) rubber",
    "poly(styrene-butadiene) rubber",
    "polyhydroxy(meth)acrylates"
  ))
  paint_classes <- c("paint", "acrylic paint", "alkyd paint", "urethane paint")
  expect_false(any(hierarchy$material_class %in% paint_classes))
  exceptions <- c(
    "other plastic"
  )
  polymer_classes <- unique(hierarchy[material_type == "plastic", material_class])
  expect_true(all(startsWith(setdiff(polymer_classes, exceptions), "poly")))
})

Try the OpenSpecy package in your browser

Any scripts or data that you put into this service are public.

OpenSpecy documentation built on Oct. 6, 2026, 1:07 a.m.