tests/testthat/test-AnalysisFunctions.R

# Tests for applicablePreviousTrials -------------------------------------------

app_prev <- setup_applicable_prev_trials()

scen_initial       <- app_prev$scen_initial
analysis_initial   <- app_prev$analysis_initial
go_initial         <- app_prev$go_initial
scen_next          <- app_prev$scen_next
analysis_with_diff <- app_prev$analysis_with_diff
go_with_diff       <- app_prev$go_with_diff
scen_next_diff     <- app_prev$scen_next_diff
method_names_prev  <- app_prev$method_names_prev
quantiles_prev     <- app_prev$quantiles_prev
n_coh_prev         <- app_prev$n_coh_prev

# ------------------------------------------------------------------
# Test: TRUE when all preconditions for reusing previous trials are met
# Input:
#   - scenario_list = scen_next
#   - method_names = method_names_prev
#   - quantiles = quantiles_prev
#   - n_cohorts = n_coh_prev
#   - calc_differences = NULL
# Behaviour:
#   - No diff columns requested; structure and methods consistent.
# Expectations:
#   - Returns a logical scalar TRUE.
# Why:
#   - Baseline positive case for applicablePreviousTrials().
# ------------------------------------------------------------------
test_that("applicablePreviousTrials returns TRUE when all conditions are met (no differences)", {
  res <- applicablePreviousTrials(
    scenario_list    = scen_next,
    method_names     = method_names_prev,  # "pooled"
    quantiles        = quantiles_prev,
    n_cohorts        = n_coh_prev,
    calc_differences = NULL
  )
  
  expect_true(is.logical(res))
  expect_length(res, 1L)
  expect_true(res)
})

# ------------------------------------------------------------------
# Test: TRUE when diff columns are requested and present in previous_analyses
# Input:
#   - scenario_list = scen_next_diff (contains p_diff_* columns)
#   - calc_differences = matrix(c(3,2)) -> p_diff_32
# Behaviour:
#   - Checks that all requested differences are available in previous quantiles.
# Expectations:
#   - Returns TRUE.
# Why:
#   - Confirms that the function correctly recognises usable previous diffs.
# ------------------------------------------------------------------
test_that("applicablePreviousTrials returns TRUE when required diff columns exist", {
  quantiles_diff <- analysis_with_diff[[1]]$analysis_parameters$quantiles
  calc_diff      <- matrix(c(3, 2), ncol = 2)   # corresponds to "p_diff_32"
  
  res <- applicablePreviousTrials(
    scenario_list    = scen_next_diff,
    method_names     = method_names_prev,
    quantiles        = quantiles_diff,
    n_cohorts        = n_coh_prev,
    calc_differences = calc_diff
  )
  
  expect_true(is.logical(res))
  expect_length(res, 1L)
  expect_true(res)
})

# ------------------------------------------------------------------
# Test: FALSE when at least one required diff column is missing
# Input:
#   - scenario_list = scen_next_diff but with a p_diff_* column removed
#   - calc_differences still asks for that difference
# Behaviour:
#   - Detection of missing p_diff_ij should invalidate reuse.
# Expectations:
#   - Returns FALSE.
# Why:
#   - Ensures robustness to incomplete previous quantile objects.
# ------------------------------------------------------------------
test_that("applicablePreviousTrials returns FALSE when required diff columns are missing", {
  scen_broken <- scen_next_diff
  
  # Remove the diff column from the first method's first trial matrix
  pq      <- scen_broken[[1]]$previous_analyses$post_quantiles
  method1 <- names(pq)[1]
  mat1    <- pq[[method1]][[1]]
  diff_cols <- grepl("^p_diff_", colnames(mat1))
  
  if (any(diff_cols)) {
    mat1 <- mat1[, !diff_cols, drop = FALSE]
    pq[[method1]][[1]] <- mat1
    scen_broken[[1]]$previous_analyses$post_quantiles <- pq
  } else {
    skip("No p_diff_* column to remove")
  }
  
  quantiles_diff <- analysis_with_diff[[1]]$analysis_parameters$quantiles
  calc_diff      <- matrix(c(3, 2), ncol = 2)   # still asking for p_diff_32
  
  res <- applicablePreviousTrials(
    scenario_list    = scen_broken,
    method_names     = method_names_prev,
    quantiles        = quantiles_diff,
    n_cohorts        = n_coh_prev,
    calc_differences = calc_diff
  )
  
  expect_true(is.logical(res))
  expect_length(res, 1L)
  expect_false(res)
})

# ------------------------------------------------------------------
# Test: FALSE when method_names differ across scenarios
# Input:
#   - Two scenarios with different names in post_quantiles
# Behaviour:
#   - Applicable only if all scenarios share identical method sets.
# Expectations:
#   - Returns FALSE.
# Why:
#   - Prevents reusing previous trials when method structure is inconsistent.
# ------------------------------------------------------------------
test_that("applicablePreviousTrials returns FALSE when method_names differ across scenarios", {
  # Copy scen_next to two scenarios, then break method-name consistency in scenario_2
  scen_multi <- scen_next
  scen_multi$scenario_2 <- scen_multi$scenario_1
  names(scen_multi) <- c("scenario_1", "scenario_2")
  class(scen_multi) <- "scenario_list"
  
  names(scen_multi$scenario_2$previous_analyses$post_quantiles) <-
    paste0("alt_", names(scen_multi$scenario_2$previous_analyses$post_quantiles))
  
  res <- applicablePreviousTrials(
    scenario_list    = scen_multi,
    method_names     = method_names_prev,
    quantiles        = quantiles_prev,
    n_cohorts        = n_coh_prev,
    calc_differences = NULL
  )
  
  expect_true(is.logical(res))
  expect_length(res, 1L)
  expect_false(res)
})


# Tests for calcDiffsMCMC ------------------------------------------------------

# ------------------------------------------------------------------
# Test: calcDiffsMCMC adds p_diff_ij columns with correct values
# Input:
#   - posterior_samples with p_1, p_2, p_3, mu
#   - calc_differences = (1,2) and (3,1)
# Behaviour:
#   - Adds p_diff_12 = p_1 - p_2, p_diff_31 = p_3 - p_1.
# Expectations:
#   - Original columns retained; new diff columns added with correct numeric values.
# Why:
#   - Validates difference computation used downstream in decision rules.
# ------------------------------------------------------------------
test_that("calcDiffsMCMC: adds correctly named difference columns with correct values", {
  posterior_samples <- cbind(
    p_1 = c(0.1, 0.2),
    p_2 = c(0.5, 0.6),
    p_3 = c(0.8, 0.9),
    mu  = c(1.0, 2.0)
  )
  
  calc_differences <- rbind(
    c(1, 2),
    c(3, 1)
  )
  
  out <- calcDiffsMCMC(
    posterior_samples = posterior_samples,
    calc_differences  = calc_differences
  )
  
  expect_true(all(c("p_1", "p_2", "p_3", "mu") %in% colnames(out)))
  expect_true(all(c("p_diff_12", "p_diff_31") %in% colnames(out)))
  
  expected_diff_12 <- posterior_samples[, "p_1"] - posterior_samples[, "p_2"]
  expected_diff_31 <- posterior_samples[, "p_3"] - posterior_samples[, "p_1"]
  
  expect_equal(out[, "p_diff_12"], expected_diff_12)
  expect_equal(out[, "p_diff_31"], expected_diff_31)
})


# Tests for performJags --------------------------------------------------------

model_file <- tempfile(fileext = ".bug")
writeLines(
  c(
    "model {",
    "  theta ~ dbeta(1, 1)",
    "  for (i in 1:N) {",
    "    y[i] ~ dbern(theta)",
    "  }",
    "}"
  ),
  con = model_file
)

data_list <- list(
  N = 20L,
  y = c(rep(1L, 14), rep(0L, 6))
)

# ------------------------------------------------------------------
# Test: performJags runs a simple Bernoulli model and returns reasonable samples
# Input:
#   - Beta(1,1) prior, 14 successes / 6 failures
# Behaviour:
#   - Posterior is Beta(15,7); posterior mean ~ 15/22.
# Expectations:
#   - Returns matrix with column "theta", expected number of rows,
#     and sample mean close to 15/22.
# Why:
#   - Integration test for JAGS wrapper and posterior extraction.
# ------------------------------------------------------------------
test_that("performJags runs a simple Bernoulli model and returns a sensible sample matrix", {
  skip_if_not_installed("rjags")
  
  n_chains <- 2L
  n_iter   <- 1000L
  n_burnin <- 100L
  
  samples <- performJags(
    data               = data_list,
    parameters_to_save = "theta",
    model_file         = model_file,
    n_chains           = n_chains,
    n_iter             = n_iter,
    n_burnin           = n_burnin
  )
  
  expect_true(is.matrix(samples))
  expect_identical(colnames(samples), "theta")
  
  expected_rows <- n_chains * (n_iter - n_burnin)
  expect_equal(nrow(samples), expected_rows)
  
  # Given 14 successes and 6 failures with a Beta(1,1) prior, the posterior is Beta(15, 7).
  # The analytic posterior mean is 15 / (15 + 7) = 15/22.
  expect_equal(mean(samples[, "theta"]), 15/22, tolerance = 0.01)
})

# ------------------------------------------------------------------
# Test: performJags handles n_burnin = 0
# Input:
#   - Same model/data, with n_burnin = 0.
# Behaviour:
#   - Uses all n_iter samples in the posterior.
# Expectations:
#   - Number of rows equals n_chains * n_iter;
#     sample mean still close to 15/22.
# Why:
#   - Verifies edge case where no explicit burn-in is requested.
# ------------------------------------------------------------------
test_that("performJags handles n_burning equals 0 correctly", {
  skip_if_not_installed("rjags")
  
  n_chains <- 2L
  n_iter   <- 1000L
  n_burnin <- 0
  
  samples <- performJags(
    data               = data_list,
    parameters_to_save = "theta",
    model_file         = model_file,
    n_chains           = n_chains,
    n_iter             = n_iter,
    n_burnin           = n_burnin
  )
  
  expect_true(is.matrix(samples))
  expect_identical(colnames(samples), "theta")
  
  expected_rows <- n_chains * (n_iter - n_burnin)
  expect_equal(nrow(samples), expected_rows)
  
  expect_equal(mean(samples[, "theta"]), 15/22, tolerance = 0.01)
})


# Tests for getPosteriors ------------------------------------------------------

# ------------------------------------------------------------------
# Test: getPosteriors basic behaviour and name cleaning (no exch weights)
# Input:
#   - Berry model prepared via prepareAnalysis, single trial scenario.
# Behaviour:
#   - Calls JAGS, returns posterior samples matrix with cleaned column names
#     (no brackets, no w_*, no "exch").
# Expectations:
#   - Matrix result, finite entries, column names stripped of indices,
#     no exNEX-specific columns.
# Why:
#   - Ensures generic posterior extraction cleans names consistently.
# ------------------------------------------------------------------
test_that("getPosteriors: basic posterior sampling and name cleaning (no exch weights)", {
  skip_if_not_installed("rjags")
  
  set.seed(123)
  
  scen_list <- simulateScenarios(
    n_subjects_list     = list(c(10, 20)),
    response_rates_list = list(c(0.5, 0.6)),
    n_trials            = 1
  )
  scen1 <- scen_list$scenario_1
  
  target_rates <- c(0.5, 0.5)
  priors       <- getPriorParameters(
    method_names = "berry",
    target_rates = target_rates
  )
  
  prep <- prepareAnalysis(
    method_name      = "berry",
    target_rates     = target_rates,
    prior_parameters = priors[["berry"]]
  )
  
  j_data <- prep$j_data
  j_data$r <- as.numeric(scen1$n_responders[1, ])
  j_data$n <- as.numeric(scen1$n_subjects[1, ])
  
  post <- getPosteriors(
    j_parameters      = prep$j_parameters,
    j_model_file      = prep$j_model_file,
    j_data            = j_data,
    n_mcmc_iterations = 20
  )
  
  expect_true(is.matrix(post))
  expect_gt(nrow(post), 0)
  expect_true(all(is.finite(post)))
  
  expect_false(any(grepl("\\[", colnames(post))))
  expect_false(any(grepl("\\]", colnames(post))))
  
  expect_false(any(grepl("^w_", colnames(post))))
  expect_false(any(grepl("exch", colnames(post))))
})

# ------------------------------------------------------------------
# Test: getPosteriors for exNEX renames exch weights and strips extras
# Input:
#   - exNEX analysis prepared via prepareAnalysis on a small scenario.
# Behaviour:
#   - Columns for exchangeability weights are renamed to w_j*;
#     exNEX internal parameters and redundant labels removed.
# Expectations:
#   - Posterior matrix, finite; no "exch" in names;
#     exactly J cohort w_* columns; no ",1" suffixes.
# Why:
#   - Confirms special handling of mixture weights in exNEX models.
# ------------------------------------------------------------------
test_that("getPosteriors: exNEX exchangeability weights are renamed to w_j and extras dropped", {
  skip_if_not_installed("rjags")
  
  set.seed(456)
  
  scen_list <- simulateScenarios(
    n_subjects_list     = list(c(10, 20)),
    response_rates_list = list(c(0.6, 0.7)),
    n_trials            = 1
  )
  scen1 <- scen_list$scenario_1
  
  target_rates <- c(0.5, 0.5)
  priors       <- getPriorParameters(
    method_names = "exnex",
    target_rates = target_rates
  )
  
  prep <- prepareAnalysis(
    method_name      = "exnex",
    target_rates     = target_rates,
    prior_parameters = priors[["exnex"]]
  )
  
  j_data <- prep$j_data
  j_data$r <- as.numeric(scen1$n_responders[1, ])
  j_data$n <- as.numeric(scen1$n_subjects[1, ])
  
  post <- getPosteriors(
    j_parameters      = prep$j_parameters,
    j_model_file      = prep$j_model_file,
    j_data            = j_data,
    n_mcmc_iterations = 20
  )
  
  expect_true(is.matrix(post))
  expect_gt(nrow(post), 0)
  expect_true(all(is.finite(post)))
  
  expect_false(any(grepl("exch", colnames(post))))
  
  w_cols <- grep("^w_", colnames(post))
  expect_equal(length(w_cols), length(j_data$n))
  
  expect_false(any(grepl(",1", colnames(post))))
})


# Tests for getPostQuantiles ---------------------------------------------------

# ------------------------------------------------------------------
# Test: pooled backend with calc_differences adds zero p_diff_* columns
# Input:
#   - scenario_data with one trial / 2 cohorts
#   - method_name = "pooled", j_data = Beta prior, calc_differences for 1↔2
# Behaviour:
#   - Pooled backend duplicates same marginal posterior for p_1 and p_2;
#     p_diff_* columns are filled with zeros by design.
# Expectations:
#   - One matrix in list; exact column/row names;
#     p_diff_* = 0; pooled Beta quantiles/mean/SD match analytic forms.
# Why:
#   - Verifies deterministic pooled implementation and diff-column handling.
# ------------------------------------------------------------------
test_that("getPostQuantiles (pooled): calc_differences adds zero columns with correct names and shapes", {
  n_subjects_mat   <- matrix(c(10, 20), nrow = 1)
  n_responders_mat <- matrix(c(3,  5),  nrow = 1)
  
  scenario_data <- list(
    n_subjects   = n_subjects_mat,
    n_responders = n_responders_mat
  )
  
  j_data    <- list(a = 1, b = 1)
  quantiles <- c(0.025, 0.5, 0.975)
  
  # Differences to compute (pooled backend fills them with zeros)
  calc_differences <- rbind(c(1, 2), c(2, 1))
  
  out <- getPostQuantiles(
    method_name       = "pooled",
    quantiles         = quantiles,
    scenario_data     = scenario_data,
    calc_differences  = calc_differences,
    j_parameters      = NULL,
    j_model_file      = NULL,
    j_data            = j_data,
    n_mcmc_iterations = 20,
    save_path         = NULL,
    save_trial        = NULL
  )
  
  expect_type(out, "list")
  expect_length(out, 1)
  
  mat <- out[[1]]
  expect_true(is.matrix(mat))
  
  expect_identical(
    colnames(mat),
    c("p_1", "p_2", "p_diff_12", "p_diff_21")
  )
  expect_identical(
    rownames(mat),
    c("2.5%", "50%", "97.5%", "Mean", "SD")
  )
  
  expect_true(all(mat[, c("p_diff_12", "p_diff_21")] == 0))
  
  r_tot   <- sum(n_responders_mat[1, ])
  n_tot   <- sum(n_subjects_mat[1, ])
  shape_1 <- j_data$a + r_tot
  shape_2 <- j_data$b + (n_tot - r_tot)
  
  expected_q  <- stats::qbeta(quantiles, shape1 = shape_1, shape2 = shape_2)
  expected_mu <- shape_1 / (shape_1 + shape_2)
  expected_sd <- sqrt((shape_1 * shape_2) / ((shape_1 + shape_2)^2 * (shape_1 + shape_2 + 1)))
  
  expect_equal(unname(mat[c("2.5%", "50%", "97.5%"), "p_1"]), expected_q, tolerance = 1e-12)
  expect_equal(unname(mat[c("2.5%", "50%", "97.5%"), "p_2"]), expected_q, tolerance = 1e-12)
  expect_equal(unname(mat["Mean", c("p_1", "p_2")]), rep(expected_mu, 2), tolerance = 1e-12)
  expect_equal(unname(mat["SD",   c("p_1", "p_2")]), rep(expected_sd, 2), tolerance = 1e-12)
})

# ------------------------------------------------------------------
# Test: stratified backend with multiple trials and calc_differences
# Input:
#   - 2 trials x 2 cohorts; method_name = "stratified"
#   - calc_differences = (1,2)
# Behaviour:
#   - Returns per-trial quantile matrices with p_1, p_2 and p_diff_12,
#     finite values for marginal posterior summaries.
# Expectations:
#   - Length of out equals number of trials; each matrix has expected
#     row/column labels and finite p_j entries.
# Why:
#   - Confirms stratified Beta backend and diff logic over multiple trials.
# ------------------------------------------------------------------
test_that("getPostQuantiles (stratified): multiple trials and calc_differences produce p_j and p_diff_*", {
  skip_if_not_installed("foreach")
  skip_if_not_installed("doRNG")
  
  foreach::registerDoSEQ()
  
  n_subjects_mat <- rbind(
    c(10, 10),
    c(20, 20)
  )
  n_responders_mat <- rbind(
    c(3, 5),
    c(6, 10)
  )
  
  scenario_data <- list(
    n_subjects   = n_subjects_mat,
    n_responders = n_responders_mat
  )
  
  j_data <- list(
    a_j = c(1, 1),
    b_j = c(1, 1)
  )
  
  quantiles        <- c(0.25, 0.5, 0.75)
  calc_differences <- matrix(c(1, 2), ncol = 2)
  
  out <- getPostQuantiles(
    method_name       = "stratified",
    quantiles         = quantiles,
    scenario_data     = scenario_data,
    calc_differences  = calc_differences,
    j_parameters      = NULL,
    j_model_file      = NULL,
    j_data            = j_data,
    n_mcmc_iterations = 20,
    save_path         = NULL,
    save_trial        = NULL
  )
  
  expect_type(out, "list")
  expect_length(out, 2)
  
  for (mat in out) {
    expect_true(is.matrix(mat))
    
    expect_setequal(
      colnames(mat),
      c("p_1", "p_2", "p_diff_12")
    )
    
    expect_setequal(
      rownames(mat),
      c(paste0(quantiles * 100, "%"), "Mean", "SD")
    )
    
    expect_true(all(is.finite(mat[, c("p_1", "p_2")])))
    
    diff_col <- mat[, "p_diff_12"]
    expect_true(is.numeric(diff_col))
  }
})


# Tests for prepareAnalysis ----------------------------------------------------

# ------------------------------------------------------------------
# Test: berry branch builds j_data and parameter names
# Input:
#   - target_rates = c(0.5, 0.7); priors from getPriorParameters("berry")
# Behaviour:
#   - j_data fields populated from priors and target_rates;
#     J equals number of cohorts; JAGS parameters and model file exist.
# Expectations:
#   - mean_mu, precision_mu, precision_tau, p_t, J consistent;
#     j_parameters = c("p","mu","tau"); model file exists.
# Why:
#   - Validates mapping from high-level Berry prior to JAGS inputs.
# ------------------------------------------------------------------
test_that("prepareAnalysis: berry branch builds correct j_data and parameters", {
  target_rates <- c(0.5, 0.7)
  priors_list  <- getPriorParameters(method_names = "berry", target_rates = target_rates)
  priors_berry <- priors_list[["berry"]]
  
  prep <- prepareAnalysis(
    method_name      = "berry",
    prior_parameters = priors_berry,
    target_rates     = target_rates
  )
  
  j_data <- prep$j_data
  
  expect_equal(j_data$mean_mu,       priors_berry$mu_mean)
  expect_equal(j_data$precision_mu,  priors_berry$mu_sd^-2)
  expect_equal(j_data$precision_tau, priors_berry$tau_scale^-2)
  expect_equal(j_data$p_t,           target_rates)
  expect_equal(j_data$J,             length(target_rates))
  
  expect_equal(prep$j_parameters, c("p", "mu", "tau"))
  expect_true(is.character(prep$j_model_file))
  expect_true(file.exists(prep$j_model_file))
})

# ------------------------------------------------------------------
# Test: exnex branch constructs mixture hyperparameters and pMix
# Input:
#   - target_rates = c(0.4,0.6,0.8); priors from getPriorParameters("exnex")
# Behaviour:
#   - j_data contains Nexch, Nmix, Nstrata, mu_mean, mu_prec,
#     tau_HN_scale, nex_mean, nex_prec and pMix that sums to 1.
# Expectations:
#   - Dimensions consistent with priors; pMix length Nexch+1 and sums to 1.
# Why:
#   - Ensures exNEX prior is correctly translated into JAGS hyperparameters.
# ------------------------------------------------------------------
test_that("prepareAnalysis: exnex branch builds mixture priors and pMix", {
  target_rates <- c(0.4, 0.6, 0.8)
  priors_list  <- getPriorParameters(method_names = "exnex", target_rates = target_rates)
  priors_exnex <- priors_list[["exnex"]]
  
  prep <- prepareAnalysis(
    method_name      = "exnex",
    prior_parameters = priors_exnex,
    target_rates     = target_rates
  )
  
  j_data <- prep$j_data
  
  Nexch   <- length(priors_exnex$mu_mean)
  Nstrata <- length(priors_exnex$mu_j)
  
  expect_equal(j_data$Nexch,   Nexch)
  expect_equal(j_data$Nmix,    Nexch + 1L)
  expect_equal(j_data$Nstrata, Nstrata)
  
  expect_equal(j_data$mu_mean, priors_exnex$mu_mean)
  expect_equal(j_data$mu_prec, priors_exnex$mu_sd^-2)
  
  expect_equal(j_data$tau_HN_scale,
               rep(priors_exnex$tau_scale, Nexch))
  
  expect_equal(j_data$nex_mean, priors_exnex$mu_j)
  expect_equal(j_data$nex_prec, priors_exnex$tau_j^-2)
  
  expect_length(j_data$pMix, Nexch + 1L)
  expect_equal(sum(j_data$pMix), 1, tolerance = 1e-12)
  
  expect_equal(prep$j_parameters, c("p", "mu", "tau", "exch"))
  expect_true(file.exists(prep$j_model_file))
})

# ------------------------------------------------------------------
# Test: exnex_adj branch includes p_target and uses adjusted model
# Input:
#   - target_rates = c(0.5,0.6); priors from getPriorParameters("exnex_adj")
# Behaviour:
#   - j_data$p_target equals target_rates; exnex_adj JAGS model used;
#     parameters include "exch".
# Expectations:
#   - p_target set; j_parameters = c("p","mu","tau","exch");
#     model file path contains "exnex_adj".
# Why:
#   - Checks adjusted exNEX prior (centered) is wired correctly.
# ------------------------------------------------------------------
test_that("prepareAnalysis: exnex_adj branch includes p_target and uses exnex_adj model", {
  target_rates <- c(0.5, 0.6)
  priors_list  <- getPriorParameters(method_names = "exnex_adj", target_rates = target_rates)
  priors       <- priors_list[["exnex_adj"]]
  
  prep <- prepareAnalysis(
    method_name      = "exnex_adj",
    prior_parameters = priors,
    target_rates     = target_rates
  )
  
  j_data <- prep$j_data
  
  expect_equal(j_data$p_target, target_rates)
  expect_equal(prep$j_parameters, c("p", "mu", "tau", "exch"))
  expect_true(grepl("exnex_adj", prep$j_model_file))
  expect_true(file.exists(prep$j_model_file))
})

# ------------------------------------------------------------------
# Test: stratified and pooled branches pass priors through and use dummy JAGS info
# Input:
#   - target_rates = (0.5,0.5); priors for "stratified" and "pooled"
# Behaviour:
#   - j_data equals prior structures;
#     j_model_file and j_parameters are placeholders.
# Expectations:
#   - Dummy values returned; priors unchanged.
# Why:
#   - These methods do not require JAGS; the interface must still be consistent.
# ------------------------------------------------------------------
test_that("prepareAnalysis: stratified and pooled use dummy JAGS info and pass priors through", {
  target_rates <- c(0.5, 0.5)
  priors_list  <- getPriorParameters(
    method_names = c("stratified", "pooled"),
    target_rates = target_rates
  )
  
  prep_strat <- prepareAnalysis(
    method_name      = "stratified",
    prior_parameters = priors_list[["stratified"]],
    target_rates     = target_rates
  )
  expect_equal(prep_strat$j_model_file, "dummy path to JAGS model")
  expect_equal(prep_strat$j_parameters, "dummy JAGS parameters")
  expect_identical(prep_strat$j_data, priors_list[["stratified"]])
  
  prep_pooled <- prepareAnalysis(
    method_name      = "pooled",
    prior_parameters = priors_list[["pooled"]],
    target_rates     = target_rates
  )
  expect_equal(prep_pooled$j_model_file, "dummy path to JAGS model")
  expect_equal(prep_pooled$j_parameters, "dummy JAGS parameters")
  expect_identical(prep_pooled$j_data, priors_list[["pooled"]])
})

# ------------------------------------------------------------------
# Test: prepareAnalysis errors for unsupported method_name
# Input:
#   - method_name = "invalid_method"
# Behaviour:
#   - Guard clause should trigger with informative message.
# Expectations:
#   - Error containing "method_name must be one of".
# Why:
#   - Protects against typos / unsupported analysis methods.
# ------------------------------------------------------------------
test_that("prepareAnalysis: invalid method_name throws an error", {
  expect_error(
    prepareAnalysis(method_name = "invalid_method"),
    "method_name must be one of"
  )
})


# Tests for getUniqueRows ------------------------------------------------------

# ------------------------------------------------------------------
# Test: getUniqueRows returns unique row combinations with full column set
# Input:
#   - Matrix with duplicate rows intermixed.
# Behaviour:
#   - Removes duplicates, preserving all columns.
# Expectations:
#   - ncol(out) == ncol(input); rows correspond to unique() of data.frame(mat).
# Why:
#   - Low-level helper for deduplicating trial patterns.
# ------------------------------------------------------------------
test_that("getUniqueRows: returns unique row combinations with correct columns", {
  mat <- rbind(
    c(1, 2),
    c(1, 2),
    c(2, 3),
    c(2, 3),
    c(4, 5)
  )
  
  out <- getUniqueRows(mat)
  
  expect_equal(ncol(out), ncol(mat))
  
  out_df <- as.data.frame(out)
  expect_equal(nrow(out_df), nrow(unique(out_df)))
  
  exp_df <- as.data.frame(unique(mat))
  
  out_ord <- out_df[do.call(order, out_df), , drop = FALSE]
  exp_ord <- exp_df[do.call(order, exp_df), , drop = FALSE]
  
  expect_equal(unname(out_ord), unname(exp_ord))
})


# Tests for getUniqueTrials ----------------------------------------------------

# ------------------------------------------------------------------
# Test: getUniqueTrials combines scenarios and returns unique responder/subject/go rows
# Input:
#   - scenario_list with controlled n_responders/n_subjects and go_decisions.
# Behaviour:
#   - Stacks all trials, attaches go_flag, and returns unique rows.
# Expectations:
#   - 5 columns (2 responders + 2 subjects + 1 go_flag);
#     no duplicate rows; equal to unique() of combined matrix.
# Why:
#   - Verifies mapping from scenario_list to a unique-trial representation.
# ------------------------------------------------------------------
test_that("getUniqueTrials: combines scenarios and returns unique responder/subject/go rows", {
  set.seed(123)
  
  scenario_list <- simulateScenarios(
    n_subjects_list     = list(c(10, 10),
                               c(10, 10)),
    response_rates_list = list(c(0.1, 0.2),
                               c(0.1, 0.2)),
    n_trials            = 2
  )
  
  # scenario_1: two identical trials (1,2 | 10,10)
  scenario_list$scenario_1$n_responders <- rbind(
    c(1, 2),
    c(1, 2)
  )
  scenario_list$scenario_1$n_subjects <- rbind(
    c(10, 10),
    c(10, 10)
  )
  
  # scenario_2: one duplicate row (1,2 | 10,10) and one distinct row (2,1 | 10,10)
  scenario_list$scenario_2$n_responders <- rbind(
    c(1, 2),
    c(2, 1)
  )
  scenario_list$scenario_2$n_subjects <- rbind(
    c(10, 10),
    c(10, 10)
  )
  
  scenario_list$scenario_1$previous_analyses <- list(
    go_decisions = cbind(c(1, 0))
  )
  scenario_list$scenario_2$previous_analyses <- list(
    go_decisions = cbind(c(1, 1))
  )
  
  class(scenario_list) <- "scenario_list"
  
  out <- getUniqueTrials(scenario_list)
  
  expect_equal(ncol(out), 5)
  
  out_df <- as.data.frame(out)
  expect_equal(nrow(out_df), nrow(unique(out_df)))
  
  all_resp <- do.call(rbind, lapply(scenario_list, function(x) x$n_responders))
  all_subj <- do.call(rbind, lapply(scenario_list, function(x) x$n_subjects))
  all_go   <- do.call(
    rbind,
    lapply(scenario_list, function(x) x$previous_analyses$go_decisions)
  )[, 1]
  
  combined <- cbind(all_resp, all_subj, go_flag = all_go)
  
  exp_df <- as.data.frame(unique(combined))
  
  out_ord <- out_df[do.call(order, out_df), , drop = FALSE]
  exp_ord <- exp_df[do.call(order, exp_df), , drop = FALSE]
  
  expect_equal(
    unname(as.matrix(out_ord)),
    unname(as.matrix(exp_ord))
  )
})


# Tests for mapUniqueTrials ----------------------------------------------------

set.seed(123)

trial_data <- createTrial(
  n_subjects   = c(10, 10),
  n_responders = c(1, 1))

analysis_list <- performAnalyses(
  scenario_list      = trial_data,
  target_rates       = rep(0.5, 2),
  method_names       = "berry",
  n_mcmc_iterations  = 20)

trials_unique <- getUniqueTrials(trial_data)
n_cohorts     <- (ncol(trials_unique) - 1L) / 2

applicable_previous_trials <- applicablePreviousTrials(
  scenario_list    = trial_data,
  method_names     = "berry",
  quantiles        = analysis_list$scenario_1$analysis_parameters$quantiles,
  n_cohorts        = n_cohorts,
  calc_differences = NULL)

expect_false(applicable_previous_trials)

if (applicable_previous_trials) {
  calc_trial_indices <- trials_unique[, ncol(trials_unique)] > 0
} else {
  calc_trial_indices <- rep(TRUE, nrow(trials_unique))
}

trials_unique_calc <- trials_unique[calc_trial_indices, -ncol(trials_unique)]
n_responders       <- trials_unique_calc[, seq_len(n_cohorts)]
n_subjects         <- trials_unique_calc[, seq_len(n_cohorts) + n_cohorts]

method_quantiles_list        <- vector(mode = "list", length = 1)
names(method_quantiles_list) <- "berry"

prior_parameters_list <- getPriorParameters(
  method_names = "berry",
  target_rates = rep(0.5, 2))

prepare_analysis <- prepareAnalysis(
  method_name       = "berry",
  target_rates      = rep(0.5, 2),
  prior_parameters  = prior_parameters_list[["berry"]])

method_quantiles_list[["berry"]] <- getPostQuantiles(
  method_name       = "berry",
  quantiles         = analysis_list$scenario_1$analysis_parameters$quantiles,
  scenario_data     = list(n_subjects   = n_subjects,
                           n_responders = n_responders),
  calc_differences  = NULL,
  j_parameters      = prepare_analysis$j_parameters,
  j_model_file      = prepare_analysis$j_model_file,
  j_data            = prepare_analysis$j_data,
  n_mcmc_iterations = 20,
  save_path         = NULL,
  save_trial        = NULL)

scenario_method_quantiles_list <- mapUniqueTrials(
  scenario_list              = trial_data,
  method_quantiles_list      = method_quantiles_list,
  trials_unique_calc         = trials_unique_calc,
  applicable_previous_trials = applicable_previous_trials)

# ------------------------------------------------------------------
# Test: with no previous trials, mapUniqueTrials copies unique quantiles per scenario
# Input:
#   - trial_data with a single scenario and no applicable_previous_trials.
# Behaviour:
#   - All trials are treated as new; their matrices equal method_quantiles_list entries.
# Expectations:
#   - scenario_1 exists; "berry" method present; each trial's matrix identical
#     to precomputed method_quantiles_list$berry[[i]].
# Why:
#   - Baseline behaviour for the mapping when no reuse is possible.
# ------------------------------------------------------------------
test_that("mapUniqueTrials: without previous trials, maps unique trial quantiles back per scenario", {
  foreach::registerDoSEQ()
  
  out <- scenario_method_quantiles_list
  
  expect_type(out, "list")
  expect_identical(names(out), "scenario_1")
  
  scen1 <- out[["scenario_1"]]
  expect_true(is.list(scen1))
  expect_true("berry" %in% names(scen1))
  
  for (i in seq_along(method_quantiles_list$berry)) {
    expect_identical(scen1$berry[[i]], method_quantiles_list$berry[[i]])
  }
})

# ------------------------------------------------------------------
# Test: with previous trials, only GO trials are updated from hash tables
# Input:
#   - One scenario with 2 trials: trial 1 GO, trial 2 NoGo.
#   - previous_analyses$post_quantiles present; new values provided for GO pattern.
# Behaviour:
#   - GO trial gets updated quantiles; NoGo trial retains its old ones.
# Expectations:
#   - scen1$berry[[1]] == new_q_go; scen1$berry[[2]] == original old_q_list[[2]].
# Why:
#   - Validates fine-grained mapping logic between unique patterns and trials.
# ------------------------------------------------------------------
test_that("mapUniqueTrials: with previous trials, only GO trials are updated from hash tables", {
  foreach::registerDoSEQ()
  set.seed(456)
  
  scene <- getScenario(
    n_subjects     = c(10, 10),
    response_rates = c(0.1, 0.2),
    n_trials       = 2
  )
  
  trial_data <- list(scenario_1 = scene)
  class(trial_data) <- "scenario_list"
  if (is.null(trial_data$scenario_1$scenario_number)) {
    trial_data$scenario_1$scenario_number <- 1L
  }
  
  go_mat <- trial_data$scenario_1$previous_analyses$go_decisions
  go_mat[, 1] <- c(TRUE, FALSE)
  trial_data$scenario_1$previous_analyses$go_decisions <- go_mat
  
  n_trials   <- trial_data$scenario_1$n_trials
  n_cohorts  <- ncol(trial_data$scenario_1$n_subjects)
  quantiles  <- c(0.025, 0.5, 0.975)
  
  prior_parameters_list <- getPriorParameters(
    method_names = "berry",
    target_rates = rep(0.5, n_cohorts)
  )
  
  prep <- prepareAnalysis(
    method_name      = "berry",
    prior_parameters = prior_parameters_list[["berry"]],
    target_rates     = rep(0.5, n_cohorts)
  )
  
  old_q_list <- vector("list", length = n_trials)
  for (i in seq_len(n_trials)) {
    n_subj_i <- trial_data$scenario_1$n_subjects[i, , drop = FALSE]
    n_resp_i <- trial_data$scenario_1$n_responders[i, , drop = FALSE]
    
    old_q_list[[i]] <- getPostQuantiles(
      method_name       = "berry",
      quantiles         = quantiles,
      scenario_data     = list(
        n_subjects   = n_subj_i,
        n_responders = n_resp_i
      ),
      calc_differences  = NULL,
      j_parameters      = prep$j_parameters,
      j_model_file      = prep$j_model_file,
      j_data            = prep$j_data,
      n_mcmc_iterations = 20,
      save_path         = NULL,
      save_trial        = NULL
    )[[1]]
  }
  
  trial_data$scenario_1$previous_analyses$post_quantiles <- list(
    berry = old_q_list
  )
  
  trials_unique <- getUniqueTrials(trial_data)
  n_cohorts_u   <- (ncol(trials_unique) - 1L) / 2L
  
  applicable_previous_trials <- TRUE
  
  calc_trial_indices <- trials_unique[, ncol(trials_unique)] > 0
  trials_unique_calc <- trials_unique[calc_trial_indices, -ncol(trials_unique), drop = FALSE]
  
  new_q_go <- old_q_list[[1]] + 1
  
  method_quantiles_list <- list(
    berry = list(new_q_go)
  )
  
  out <- mapUniqueTrials(
    scenario_list              = trial_data,
    method_quantiles_list      = method_quantiles_list,
    trials_unique_calc         = trials_unique_calc,
    applicable_previous_trials = applicable_previous_trials
  )
  
  expect_type(out, "list")
  expect_identical(names(out), "scenario_1")
  
  scen1 <- out[["scenario_1"]]
  expect_true(is.list(scen1))
  expect_true("berry" %in% names(scen1))
  
  expect_identical(scen1$berry[[1]], new_q_go)
  expect_identical(scen1$berry[[2]], old_q_list[[2]])
})

## Test: mapUniqueTrials with pooled backend preserves naive per-trial quantiles
## Input:
##   - One scenario with several trials and 2 cohorts (generated via getScenario()).
##   - trials_unique_calc derived from getUniqueTrials(), with
##     applicable_previous_trials = FALSE so that all unique patterns are treated
##     as newly analysed.
##   - method_quantiles_list for method "pooled" containing posterior quantile
##     matrices computed only for the unique trial patterns using getPostQuantiles().
##
## Naive reference:
##   - For each original trial i (row i of n_responders, n_subjects), we call
##     getPostQuantiles(method_name = "pooled", ...) directly and store the
##     resulting single-trial quantile matrix in naive_list[[i]].
##
## Expected output:
##   - mapUniqueTrials returns a list with one element named "scenario_1".
##   - scenario_1$pooled is a list of length equal to the original number of trials.
##   - For each trial i, scenario_1$pooled[[i]] is exactly equal to
##     naive_list[[i]] (the per-trial quantile matrix from the naive analysis).
##
## Why this test:
##   - For the pooled design, getPostQuantiles() is deterministic given
##     (n_responders, n_subjects) and j_data.
##   - mapUniqueTrials must use hashing + unique trial patterns to avoid
##     redundant computations, but still reproduce the same per-trial quantiles
##     as the naive approach that analyses each trial separately.

test_that("mapUniqueTrials: pooled backend preserves naive per-trial Beta quantiles", {
  skip_if_not_installed("foreach")
  foreach::registerDoSEQ()
  set.seed(123)
  
  scene <- getScenario(
    n_subjects     = c(10, 20),
    response_rates = c(0.4, 0.6),
    n_trials       = 10
  )
  
  if (is.null(scene$scenario_number)) {
    scene$scenario_number <- 1L
  }
  
  scenario_list <- list(scenario_1 = scene)
  class(scenario_list) <- "scenario_list"
  
  n_subj   <- scene$n_subjects
  n_resp   <- scene$n_responders
  n_trials <- nrow(n_subj)
  n_coh    <- ncol(n_subj)
  
  trials_unique <- getUniqueTrials(scenario_list)
  n_coh_u       <- (ncol(trials_unique) - 1L) / 2L
  expect_equal(n_coh_u, n_coh)
  
  applicable_previous_trials <- FALSE
  
  calc_trial_indices <- rep(TRUE, nrow(trials_unique))
  trials_unique_calc <- trials_unique[calc_trial_indices, -ncol(trials_unique), drop = FALSE]
  
  n_resp_unique <- trials_unique_calc[, seq_len(n_coh),         drop = FALSE]
  n_subj_unique <- trials_unique_calc[, seq_len(n_coh) + n_coh, drop = FALSE]
  
  j_data    <- list(a = 1, b = 1)
  quantiles <- c(0.025, 0.5, 0.975)
  
  unique_quantiles <- getPostQuantiles(
    method_name       = "pooled",
    quantiles         = quantiles,
    scenario_data     = list(
      n_subjects   = n_subj_unique,
      n_responders = n_resp_unique
    ),
    calc_differences  = NULL,
    j_parameters      = NULL,
    j_model_file      = NULL,
    j_data            = j_data,
    n_mcmc_iterations = 20,
    save_path         = NULL,
    save_trial        = NULL
  )
  
  naive_list <- vector("list", length = n_trials)
  for (i in seq_len(n_trials)) {
    n_subj_i <- n_subj[i, , drop = FALSE]
    n_resp_i <- n_resp[i, , drop = FALSE]
    
    naive_list[[i]] <- getPostQuantiles(
      method_name       = "pooled",
      quantiles         = quantiles,
      scenario_data     = list(
        n_subjects   = n_subj_i,
        n_responders = n_resp_i
      ),
      calc_differences  = NULL,
      j_parameters      = NULL,
      j_model_file      = NULL,
      j_data            = j_data,
      n_mcmc_iterations = 20,
      save_path         = NULL,
      save_trial        = NULL
    )[[1]]
  }
  
  method_quantiles_list <- list(
    pooled = unique_quantiles
  )
  
  out <- mapUniqueTrials(
    scenario_list              = scenario_list,
    method_quantiles_list      = method_quantiles_list,
    trials_unique_calc         = trials_unique_calc,
    applicable_previous_trials = applicable_previous_trials
  )
  
  expect_type(out, "list")
  expect_identical(names(out), "scenario_1")
  
  scen1_out <- out[["scenario_1"]]
  expect_true(is.list(scen1_out))
  expect_true("pooled" %in% names(scen1_out))
  expect_equal(length(scen1_out$pooled), n_trials)
  
  for (i in seq_len(n_trials)) {
    expect_equal(
      scen1_out$pooled[[i]],
      naive_list[[i]],
      tolerance = 1e-12
    )
  }
})

# Tests for posteriors2Quantiles ----------------------------------------------

# ------------------------------------------------------------------
# Test: posteriors2Quantiles computes requested quantiles, mean and SD
# Input:
#   - Normal samples theta ~ N(2, 3^2); 5000 draws.
# Behaviour:
#   - Returns a matrix with rows: quantiles, Mean, SD.
# Expectations:
#   - Row/column names as specified; values close to empirical summaries.
# Why:
#   - Core summarisation helper for posterior draws.
# ------------------------------------------------------------------
test_that("posteriors2Quantiles: computes quantiles, mean, and sd for each column", {
  set.seed(123)
  
  theta <- rnorm(5000, mean = 2, sd = 3)
  post  <- cbind(theta = theta)
  
  quantiles <- c(0.25, 0.5, 0.75)
  
  out <- posteriors2Quantiles(
    quantiles  = quantiles,
    posteriors = post
  )
  
  expect_true(is.matrix(out))
  expect_identical(colnames(out), "theta")
  expect_identical(rownames(out),
                   c("25%", "50%", "75%", "Mean", "SD"))
  
  exp_q <- stats::quantile(theta, probs = quantiles)
  expect_equal(out[c("25%", "50%", "75%"), "theta"],
               exp_q,
               tolerance = 1e-12)
  
  expect_equal(out["Mean", "theta"], mean(theta), tolerance = 1e-12)
  expect_equal(out["SD",   "theta"], stats::sd(theta), tolerance = 1e-12)
})


# Tests for performAnalyses ----------------------------------------------------

# Shared scenario_list for performAnalyses tests
scenario_list_pa <- make_scenario_list_pa()

prior_parameters_list <- getPriorParameters(
  method_names = c("pooled", "exnex"),
  target_rates = c(0.5, 0.5, 0.5)
)

# ------------------------------------------------------------------
# Test: performAnalyses returns a well-formed analysis_list
# Input:
#   - scenario_list_pa; methods pooled + exnex; supplied priors.
# Behaviour:
#   - One analysis entry per scenario with quantiles_list, scenario_data,
#     and analysis_parameters.
# Expectations:
#   - S3 class "analysis_list"; names "scenario_k"; scenario_data unchanged.
# Why:
#   - High-level sanity check of analysis pipeline.
# ------------------------------------------------------------------
test_that("performAnalyses returns a well-formed analysis_list", {
  res <- performAnalyses(
    scenario_list         = scenario_list_pa,
    target_rates          = c(0.5, 0.5, 0.5),
    method_names          = c("pooled", "exnex"),
    prior_parameters_list = prior_parameters_list,
    n_mcmc_iterations     = 20,
    verbose               = FALSE
  )
  
  expect_s3_class(res, "analysis_list")
  expect_equal(length(res), length(scenario_list_pa))
  expect_identical(names(res), paste0("scenario_", sapply(scenario_list_pa, `[[`, "scenario_number")))
  
  scen1 <- res[[1]]
  expect_true(is.list(scen1))
  expect_true(all(c("quantiles_list", "scenario_data", "analysis_parameters") %in% names(scen1)))
  
  expect_identical(scen1$scenario_data, scenario_list_pa[[1]])
})

# ------------------------------------------------------------------
# Test: performAnalyses sorts method_names and stores in analysis_parameters
# Input:
#   - method_names in unsorted order: stratified, berry, pooled.
# Behaviour:
#   - Internally sorts method_names; quantiles_list and analysis_parameters
#     follow sorted order.
# Expectations:
#   - scen1$analysis_parameters$method_names sorted; quantiles_list names match.
# Why:
#   - Ensures deterministic method ordering across analyses.
# ------------------------------------------------------------------
test_that("performAnalyses sorts method_names and stores them in analysis_parameters", {
  res <- performAnalyses(
    scenario_list       = scenario_list_pa,
    target_rates        = c(0.5, 0.5, 0.5),
    method_names        = c("stratified", "berry", "pooled"),
    n_mcmc_iterations   = 20,
    verbose             = FALSE
  )
  
  scen1 <- res[[1]]
  
  expected_sorted <- sort(c("stratified", "berry", "pooled"))
  expect_identical(scen1$analysis_parameters$method_names, expected_sorted)
  
  expect_true(is.list(scen1$quantiles_list))
  expect_identical(names(scen1$quantiles_list), expected_sorted)
})

# ------------------------------------------------------------------
# Test: performAnalyses builds quantiles from defaults + evidence_levels
# Input:
#   - evidence_levels = c(0.1,0.2,0.3)
# Behaviour:
#   - analysis_parameters$quantiles is union of reversed evidence_levels
#     and default quantiles, sorted.
# Expectations:
#   - Numeric, sorted; contains all default and evidence-based quantiles.
# Why:
#   - Confirms evidence_levels are incorporated into posterior summaries.
# ------------------------------------------------------------------
test_that("performAnalyses constructs quantiles from defaults and evidence_levels", {
  ev_levels <- c(0.1, 0.2, 0.3)
  
  res <- performAnalyses(
    scenario_list       = scenario_list_pa,
    evidence_levels     = ev_levels,
    target_rates        = c(0.5, 0.5, 0.5),
    method_names        = c("pooled"),
    n_mcmc_iterations   = 20,
    verbose             = FALSE
  )
  
  q <- res[[1]]$analysis_parameters$quantiles
  expect_true(is.numeric(q))
  expect_false(is.unsorted(q))
  
  defaults <- c(0.025, 0.05, 0.5, 0.8, 0.9, 0.95, 0.975)
  expected_q <- sort(unique(round(1 - c(defaults, ev_levels), 9)))
  
  expect_true(all(expected_q %in% q))
})

# ------------------------------------------------------------------
# Test: performAnalyses auto-fills prior_parameters_list when omitted
# Input:
#   - method_names = c("berry","pooled"), prior_parameters_list = NULL.
# Behaviour:
#   - Internally calls getPriorParameters and stores result in analysis_parameters.
# Expectations:
#   - Non-null priors; list; contains both methods as names.
# Why:
#   - Convenience behaviour for users not providing priors explicitly.
# ------------------------------------------------------------------
test_that("performAnalyses fills prior_parameters_list when not supplied", {
  methods <- c("berry", "pooled")
  
  res <- performAnalyses(
    scenario_list         = scenario_list_pa,
    target_rates          = c(0.5, 0.5, 0.5),
    method_names          = methods,
    prior_parameters_list = NULL,
    n_mcmc_iterations     = 20,
    verbose               = FALSE
  )
  
  priors <- res[[1]]$analysis_parameters$prior_parameters_list
  
  expect_false(is.null(priors))
  expect_true(is.list(priors))
  expect_true(all(methods %in% names(priors)))
})

# ------------------------------------------------------------------
# Test: performAnalyses prints a progress message when verbose = TRUE
# Input:
#   - verbose = TRUE
# Behaviour:
#   - Progress message "Performing Analyses" printed to console.
# Expectations:
#   - expect_message detects that substring.
# Why:
#   - Confirms user-facing verbosity option works.
# ------------------------------------------------------------------
test_that("performAnalyses prints a progress message when verbose = TRUE", {
  expect_message(
    performAnalyses(
      scenario_list       = scenario_list_pa,
      target_rates        = c(0.5, 0.5, 0.5),
      method_names        = c("pooled"),
      n_mcmc_iterations   = 20,
      verbose             = TRUE
    ),
    "Performing Analyses"
  )
})


# Tests for loadAnalyses -------------------------------------------------------

# ------------------------------------------------------------------
# Test: loadAnalyses reads saved analyses and sets class/names
# Input:
#   - Two RDS files matching analysis_data_1_1 and analysis_data_2_2.
# Behaviour:
#   - Reads into an analysis_list, named scenario_1, scenario_2.
# Expectations:
#   - S3 class "analysis_list"; names c("scenario_1","scenario_2");
#     contents identical to original dummies.
# Why:
#   - Confirms correct file naming convention and object reconstruction.
# ------------------------------------------------------------------
test_that("loadAnalyses: loads saved analyses and sets class/names correctly", {
  tmpdir <- tempdir()
  
  scen_nums <- c(1, 2)
  anal_nums <- c(1, 2)
  
  dummy1 <- list(foo = 1)
  dummy2 <- list(bar = 2)
  
  saveRDS(dummy1, file = file.path(tmpdir, "analysis_data_1_1.rds"))
  saveRDS(dummy2, file = file.path(tmpdir, "analysis_data_2_2.rds"))
  
  loaded <- loadAnalyses(
    scenario_numbers = scen_nums,
    analysis_numbers = anal_nums,
    load_path        = tmpdir
  )
  
  expect_s3_class(loaded, "analysis_list")
  expect_identical(names(loaded), c("scenario_1", "scenario_2"))
  
  expect_identical(loaded[[1]], dummy1)
  expect_identical(loaded[[2]], dummy2)
})

# ------------------------------------------------------------------
# Test: loadAnalyses validates scenario_numbers as positive integers
# Input:
#   - scenario_numbers = "a" or c(0,1).
# Behaviour:
#   - Non-integer or non-positive scenario_numbers should fail.
# Expectations:
#   - Errors mentioning "scenario_numbers".
# Why:
#   - Enforces basic integrity on index arguments.
# ------------------------------------------------------------------
test_that("loadAnalyses: scenario_numbers must be positive integers", {
  tmpdir <- tempdir()
  
  expect_error(
    loadAnalyses(scenario_numbers = "a", load_path = tmpdir),
    "scenario_numbers"
  )
  
  expect_error(
    loadAnalyses(scenario_numbers = c(0, 1), load_path = tmpdir),
    "scenario_numbers"
  )
})

# ------------------------------------------------------------------
# Test: loadAnalyses validates analysis_numbers length and positivity
# Input:
#   - analysis_numbers length mismatched to scenario_numbers or non-positive.
# Behaviour:
#   - Should error with appropriate message.
# Expectations:
#   - Errors mentioning "analysis_numbers".
# Why:
#   - Prevents ambiguous mapping between scenarios and analyses.
# ------------------------------------------------------------------
test_that("loadAnalyses: analysis_numbers must be positive integers of same length", {
  tmpdir <- tempdir()
  
  expect_error(
    loadAnalyses(
      scenario_numbers = c(1, 2),
      analysis_numbers = c(1),
      load_path        = tmpdir
    ),
    "analysis_numbers"
  )
  
  expect_error(
    loadAnalyses(
      scenario_numbers = c(1, 2),
      analysis_numbers = c(1, -1),
      load_path        = tmpdir
    ),
    "analysis_numbers"
  )
})

# ------------------------------------------------------------------
# Test: loadAnalyses requires load_path be a single character string
# Input:
#   - load_path = 123 or c("a","b").
# Behaviour:
#   - Type and length checks must fail.
# Expectations:
#   - Errors mentioning "load_path".
# Why:
#   - Guards against invalid path inputs.
# ------------------------------------------------------------------
test_that("loadAnalyses: load_path must be a single character string", {
  expect_error(
    loadAnalyses(scenario_numbers = 1, load_path = 123),
    "load_path"
  )
  expect_error(
    loadAnalyses(scenario_numbers = 1, load_path = c("a", "b")),
    "load_path"
  )
})


# Tests for printAnalyses ------------------------------------------------------

# ------------------------------------------------------------------
# Test: print.analysis_list prints header, scenario blocks, labels, and estimates
# Input:
#   - analyses from pooled method on 2 scenarios; estimates via getEstimates().
# Behaviour:
#   - print() shows header, per-scenario listing, "Pooled" label,
#     and numeric summary values like mean and SD.
# Expectations:
#   - Output contains correct header; scenario lines; method label;
#     and numeric values matching rounded estimates.
# Why:
#   - Verifies user-facing summary of analysis_list.
# ------------------------------------------------------------------
test_that("print.analysis_list: prints header, scenario blocks, method label, and numeric estimates", {
  set.seed(123)
  
  scen <- simulateScenarios(
    n_subjects_list     = list(c(10, 20),
                               c(10, 20)),
    response_rates_list = list(c(0.3, 0.6),
                               c(0.4, 0.7)),
    n_trials            = 50
  )
  
  analyses <- performAnalyses(
    scenario_list      = scen,
    method_names       = "pooled",
    target_rates       = c(0.5, 0.5),
    n_mcmc_iterations  = 20,
    verbose            = FALSE
  )
  
  est_list <- getEstimates(analyses)
  
  expect_true(is.list(est_list[[1]]))
  est_mat1 <- est_list[[1]][[1]]
  
  mean_p1 <- round(est_mat1["p_1", "Mean"], digits = 2)
  sd_p1   <- round(est_mat1["p_1", "SD"],   digits = 2)
  
  out <- capture.output(print(analyses))
  flat_out <- paste(out, collapse = " ")
  
  expect_true(
    any(grepl("analysis_list of 2 scenarios with 1 method", out))
  )
  
  expect_true(
    any(grepl("^  - scenario_1", out))
  )
  expect_true(
    any(grepl("^  - scenario_2", out))
  )
  
  expect_true(
    any(grepl("Pooled", out))
  )
  
  mean_str <- sprintf("%.2f", mean_p1)
  sd_str   <- sprintf("%.2f", sd_p1)
  
  expect_true(
    grepl(mean_str, flat_out)
  )
  expect_true(
    grepl(sd_str, flat_out)
  )
  
  expect_true(
    any(grepl("MCMC iterationns per BHM method", out))
  )
  expect_true(
    any(grepl("Available evidence levels:", out))
  )
})

# ------------------------------------------------------------------
# Test: print.analysis_list shows distinct scenario-specific estimates
# Input:
#   - Two scenarios with different response rates, pooled method.
# Behaviour:
#   - Printed output must include different means for p_1 per scenario.
# Expectations:
#   - Output includes both rounded means; these means differ numerically.
# Why:
#   - Ensures print summarises scenario-specific posterior differences.
# ------------------------------------------------------------------
test_that("print.analysis_list: multiple scenarios print distinct scenario-specific estimates", {
  set.seed(456)
  
  n_subj <- c(10, 20)
  rr1    <- c(0.3, 0.6)
  rr2    <- c(0.7, 0.2)
  
  scen_multi <- simulateScenarios(
    n_subjects_list     = list(n_subj, n_subj),
    response_rates_list = list(rr1, rr2),
    n_trials            = 30
  )
  
  analyses_multi <- performAnalyses(
    scenario_list      = scen_multi,
    method_names       = "pooled",
    target_rates       = c(0.5, 0.5),
    n_mcmc_iterations  = 20,
    verbose            = FALSE
  )
  
  est_multi <- getEstimates(analyses_multi)
  
  expect_true(is.list(est_multi[[1]]))
  m1 <- est_multi[[1]][[1]]
  m2 <- est_multi[[1]][[2]]
  
  mean1 <- round(m1["p_1", "Mean"], 2)
  mean2 <- round(m2["p_1", "Mean"], 2)
  
  out <- capture.output(print(analyses_multi))
  flat_out <- paste(out, collapse = " ")
  
  expect_true(
    any(grepl("analysis_list of 2 scenarios with 1 method", out))
  )
  
  expect_true(any(grepl("^  - scenario_1", out)))
  expect_true(any(grepl("^  - scenario_2", out)))
  
  mean1_str <- sprintf("%.2f", mean1)
  mean2_str <- sprintf("%.2f", mean2)
  
  expect_true(
    grepl(mean1_str, flat_out)
  )
  expect_true(
    grepl(mean2_str, flat_out)
  )
  
  expect_false(
    isTRUE(all.equal(mean1, mean2))
  )
})

# ------------------------------------------------------------------
# Test: digits argument controls numeric precision in printed output
# Input:
#   - digits = 2 and digits = 4; same analyses object.
# Behaviour:
#   - Numeric summaries printed with specified precision.
# Expectations:
#   - Output with digits=2 contains 2-decimal format; digits=4 contains 4-decimal format.
# Why:
#   - Confirms users can tune displayed precision.
# ------------------------------------------------------------------
test_that("print.analysis_list: digits argument controls printed numeric precision", {
  set.seed(789)
  
  scen <- simulateScenarios(
    n_subjects_list     = list(c(10, 20),
                               c(10, 20)),
    response_rates_list = list(c(0.33, 0.66),
                               c(0.33, 0.66)),
    n_trials            = 40
  )
  
  analyses <- performAnalyses(
    scenario_list      = scen,
    method_names       = "pooled",
    target_rates       = c(0.5, 0.5),
    n_mcmc_iterations  = 20,
    verbose            = FALSE
  )
  
  est_list <- getEstimates(analyses)
  est_obj  <- est_list[[1]]
  expect_true(is.list(est_obj))
  est_mat1 <- est_obj[[1]]
  
  mean_p1 <- est_mat1["p_1", "Mean"]
  
  out_2 <- capture.output(print(analyses, digits = 2))
  out_4 <- capture.output(print(analyses, digits = 4))
  
  flat_2 <- paste(out_2, collapse = " ")
  flat_4 <- paste(out_4, collapse = " ")
  
  fmt2 <- sprintf("%.2f", round(mean_p1, 2))
  fmt4 <- sprintf("%.4f", round(mean_p1, 4))
  
  expect_true(
    grepl(fmt2, flat_2)
  )
  
  expect_true(
    grepl(fmt4, flat_4)
  )
})


# Tests for saveAnalyses -------------------------------------------------------

# ------------------------------------------------------------------
# Test: saveAnalyses writes analysis_list to disk and loadAnalyses reads it back
# Input:
#   - analyses from pooled method, multiple scenarios; temp directory.
# Behaviour:
#   - saveAnalyses returns path and scenario/analysis numbers;
#     files exist; loadAnalyses reproduces original object.
# Expectations:
#   - info list correctly populated; RDS files present;
#     loaded analysis_list identical in length/names to original.
# Why:
#   - Round-trip test for persistence layer.
# ------------------------------------------------------------------
test_that("saveAnalyses: saves analysis_list to disk and loadAnalyses can read it back", {
  set.seed(101)
  
  scen <- simulateScenarios(
    n_subjects_list     = list(c(10, 20),
                               c(15, 25)),
    response_rates_list = list(c(0.3, 0.6),
                               c(0.5, 0.8)),
    n_trials            = 20
  )
  
  analyses <- performAnalyses(
    scenario_list      = scen,
    method_names       = "pooled",
    target_rates       = c(0.5, 0.5),
    n_mcmc_iterations  = 20,
    verbose            = FALSE
  )
  
  tmp_dir <- tempdir()
  
  info <- saveAnalyses(analyses, save_path = tmp_dir)
  
  expect_true(is.list(info))
  expect_equal(length(info$scenario_numbers), length(analyses))
  expect_equal(length(info$analysis_numbers), length(analyses))
  expect_identical(info$path, tmp_dir)
  
  internal_scen <- vapply(
    analyses,
    function(x) x$scenario_data$scenario_number,
    FUN.VALUE = numeric(1)
  )
  expect_equal(info$scenario_numbers, internal_scen)
  
  for (i in seq_along(info$scenario_numbers)) {
    f <- file.path(
      tmp_dir,
      paste0("analysis_data_",
             info$scenario_numbers[i], "_",
             info$analysis_numbers[i], ".rds")
    )
    expect_true(file.exists(f))
  }
  
  loaded <- loadAnalyses(
    scenario_numbers = info$scenario_numbers,
    analysis_numbers = info$analysis_numbers,
    load_path        = info$path
  )
  
  expect_s3_class(loaded, "analysis_list")
  expect_identical(length(loaded), length(analyses))
  expect_identical(names(loaded), names(analyses))
})

# ------------------------------------------------------------------
# Test: saveAnalyses requires an object of class 'analysis_list'
# Input:
#   - list(a = 1) instead of analysis_list.
# Behaviour:
#   - Class check must fail with informative message.
# Expectations:
#   - Error mentioning "analysis_list".
# Why:
#   - Prevents misuse with arbitrary lists.
# ------------------------------------------------------------------
test_that("saveAnalyses: non-analysis_list input triggers a class error", {
  expect_error(
    saveAnalyses(list(a = 1)),
    "analysis_list",
    ignore.case = TRUE
  )
})

# ------------------------------------------------------------------
# Test: saveAnalyses requires save_path to be a length-1 character vector
# Input:
#   - save_path = c("a","b").
# Behaviour:
#   - Argument validation should fail.
# Expectations:
#   - Error mentioning "save_path".
# Why:
#   - Enforces a single destination directory.
# ------------------------------------------------------------------
test_that("saveAnalyses: save_path must be a character vector of length 1", {
  set.seed(202)
  
  scen <- simulateScenarios(
    n_subjects_list     = list(c(10, 20)),
    response_rates_list = list(c(0.3, 0.6)),
    n_trials            = 10
  )
  
  analyses <- performAnalyses(
    scenario_list      = scen,
    method_names       = "pooled",
    target_rates       = c(0.5, 0.5),
    n_mcmc_iterations  = 20,
    verbose            = FALSE
  )
  
  expect_error(
    saveAnalyses(analyses, save_path = c("a", "b")),
    "save_path",
    ignore.case = TRUE
  )
})

Try the bhmbasket package in your browser

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

bhmbasket documentation built on Feb. 21, 2026, 9:07 a.m.