# Combined tests from unit/
# Auto-generated by workspace refactor script

# ===== BEGIN unit/test-dispatch-separation.R =====
test_that("scalar vs vector dispatch separation is enforced", {

  spec <- list(
    meta = list(backend = "sb", kernel = "gamma", GPD = FALSE),
    dispatch = list(backend = "sb", GPD = FALSE),
    kernel = list(key = "gamma")
  )

  scalar <- .get_dispatch_scalar(spec)
  expect_false(isTRUE(attr(scalar$d, "vectorized_wrapper")))
  expect_false(isTRUE(attr(scalar$p, "vectorized_wrapper")))
  expect_false(isTRUE(attr(scalar$q, "vectorized_wrapper")))
  expect_false(isTRUE(attr(scalar$r, "vectorized_wrapper")))

  vec <- .get_dispatch(spec)
  expect_true(isTRUE(attr(vec$d, "vectorized_wrapper")))
  expect_true(isTRUE(attr(vec$p, "vectorized_wrapper")))
  expect_true(isTRUE(attr(vec$q, "vectorized_wrapper")))
  expect_true(isTRUE(attr(vec$r, "vectorized_wrapper")))
})

# ===== END unit/test-dispatch-separation.R =====

# ===== BEGIN unit/test-distributions.R =====
# test-distributions.R
# Consolidated per-kernel distribution integration tests (Tier B)
# Merged from: test-amoroso.R, test-cauchy.R, test-gamma.R, test-invgauss.R,
#             test-laplace.R, test-lognormal.R, test-normal.R

if (!exists(".run_predict_case")) {
  helper_path <- file.path("tests", "testthat", "helper-03-predict-helpers.R")
  if (file.exists(helper_path)) source(helper_path)
}

# =============================================================================
# Amoroso kernel
# =============================================================================
test_that("SB amoroso: no GPD, no X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("SB amoroso: no GPD, no X", "amoroso", "sb", FALSE, FALSE)
  expect_true(res$ok, info = res$msg)
})

test_that("SB amoroso: GPD, no X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("SB amoroso: GPD, no X", "amoroso", "sb", TRUE, FALSE)
  expect_true(res$ok, info = res$msg)
})

test_that("SB amoroso: no GPD, with X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("SB amoroso: no GPD, with X", "amoroso", "sb", FALSE, TRUE)
  expect_true(res$ok, info = res$msg)
})

test_that("SB amoroso: GPD, with X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("SB amoroso: GPD, with X", "amoroso", "sb", TRUE, TRUE)
  expect_true(res$ok, info = res$msg)
})

test_that("CRP amoroso: no GPD, no X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("CRP amoroso: no GPD, no X", "amoroso", "crp", FALSE, FALSE)
  expect_true(res$ok, info = res$msg)
})

test_that("CRP amoroso: GPD, no X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("CRP amoroso: GPD, no X", "amoroso", "crp", TRUE, FALSE)
  expect_true(res$ok, info = res$msg)
})

test_that("CRP amoroso: no GPD, with X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("CRP amoroso: no GPD, with X", "amoroso", "crp", FALSE, TRUE)
  expect_true(res$ok, info = res$msg)
})

test_that("CRP amoroso: GPD, with X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("CRP amoroso: GPD, with X", "amoroso", "crp", TRUE, TRUE)
  expect_true(res$ok, info = res$msg)
})

# =============================================================================
# Cauchy kernel
# =============================================================================
test_that("SB cauchy: no GPD, no X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("SB cauchy: no GPD, no X", "cauchy", "sb", FALSE, FALSE)
  expect_true(res$ok, info = res$msg)
})

test_that("SB cauchy: GPD, no X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("SB cauchy: GPD, no X", "cauchy", "sb", TRUE, FALSE)
  expect_true(res$ok, info = res$msg)
})

test_that("SB cauchy: no GPD, with X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("SB cauchy: no GPD, with X", "cauchy", "sb", FALSE, TRUE)
  expect_true(res$ok, info = res$msg)
})

test_that("SB cauchy: GPD, with X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("SB cauchy: GPD, with X", "cauchy", "sb", TRUE, TRUE)
  expect_true(res$ok, info = res$msg)
})

test_that("CRP cauchy: no GPD, no X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("CRP cauchy: no GPD, no X", "cauchy", "crp", FALSE, FALSE)
  expect_true(res$ok, info = res$msg)
})

test_that("CRP cauchy: GPD, no X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("CRP cauchy: GPD, no X", "cauchy", "crp", TRUE, FALSE)
  expect_true(res$ok, info = res$msg)
})

test_that("CRP cauchy: no GPD, with X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("CRP cauchy: no GPD, with X", "cauchy", "crp", FALSE, TRUE)
  expect_true(res$ok, info = res$msg)
})

test_that("CRP cauchy: GPD, with X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("CRP cauchy: GPD, with X", "cauchy", "crp", TRUE, TRUE)
  expect_true(res$ok, info = res$msg)
})

# =============================================================================
# Gamma kernel
# =============================================================================
test_that("SB gamma: no GPD, no X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("SB gamma: no GPD, no X", "gamma", "sb", FALSE, FALSE)
  expect_true(res$ok, info = res$msg)
})

test_that("SB gamma: GPD, no X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("SB gamma: GPD, no X", "gamma", "sb", TRUE, FALSE)
  expect_true(res$ok, info = res$msg)
})

test_that("SB gamma: no GPD, with X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("SB gamma: no GPD, with X", "gamma", "sb", FALSE, TRUE)
  expect_true(res$ok, info = res$msg)
})

test_that("SB gamma: GPD, with X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("SB gamma: GPD, with X", "gamma", "sb", TRUE, TRUE)
  expect_true(res$ok, info = res$msg)
})

test_that("CRP gamma: no GPD, no X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("CRP gamma: no GPD, no X", "gamma", "crp", FALSE, FALSE)
  expect_true(res$ok, info = res$msg)
})

test_that("CRP gamma: GPD, no X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("CRP gamma: GPD, no X", "gamma", "crp", TRUE, FALSE)
  expect_true(res$ok, info = res$msg)
})

test_that("CRP gamma: no GPD, with X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("CRP gamma: no GPD, with X", "gamma", "crp", FALSE, TRUE)
  expect_true(res$ok, info = res$msg)
})

test_that("CRP gamma: GPD, with X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("CRP gamma: GPD, with X", "gamma", "crp", TRUE, TRUE)
  expect_true(res$ok, info = res$msg)
})

# =============================================================================
# InvGauss kernel
# =============================================================================
test_that("SB invgauss: no GPD, no X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("SB invgauss: no GPD, no X", "invgauss", "sb", FALSE, FALSE)
  expect_true(res$ok, info = res$msg)
})

test_that("SB invgauss: GPD, no X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("SB invgauss: GPD, no X", "invgauss", "sb", TRUE, FALSE)
  expect_true(res$ok, info = res$msg)
})

test_that("SB invgauss: no GPD, with X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("SB invgauss: no GPD, with X", "invgauss", "sb", FALSE, TRUE)
  expect_true(res$ok, info = res$msg)
})

test_that("SB invgauss: GPD, with X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("SB invgauss: GPD, with X", "invgauss", "sb", TRUE, TRUE)
  expect_true(res$ok, info = res$msg)
})

test_that("CRP invgauss: no GPD, no X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("CRP invgauss: no GPD, no X", "invgauss", "crp", FALSE, FALSE)
  expect_true(res$ok, info = res$msg)
})

test_that("CRP invgauss: GPD, no X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("CRP invgauss: GPD, no X", "invgauss", "crp", TRUE, FALSE)
  expect_true(res$ok, info = res$msg)
})

test_that("CRP invgauss: no GPD, with X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("CRP invgauss: no GPD, with X", "invgauss", "crp", FALSE, TRUE)
  expect_true(res$ok, info = res$msg)
})

test_that("CRP invgauss: GPD, with X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("CRP invgauss: GPD, with X", "invgauss", "crp", TRUE, TRUE)
  expect_true(res$ok, info = res$msg)
})

# =============================================================================
# Laplace kernel
# =============================================================================
test_that("SB laplace: no GPD, no X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("SB laplace: no GPD, no X", "laplace", "sb", FALSE, FALSE)
  expect_true(res$ok, info = res$msg)
})

test_that("SB laplace: GPD, no X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("SB laplace: GPD, no X", "laplace", "sb", TRUE, FALSE)
  expect_true(res$ok, info = res$msg)
})

test_that("SB laplace: no GPD, with X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("SB laplace: no GPD, with X", "laplace", "sb", FALSE, TRUE)
  expect_true(res$ok, info = res$msg)
})

test_that("SB laplace: GPD, with X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("SB laplace: GPD, with X", "laplace", "sb", TRUE, TRUE)
  expect_true(res$ok, info = res$msg)
})

test_that("CRP laplace: no GPD, no X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("CRP laplace: no GPD, no X", "laplace", "crp", FALSE, FALSE)
  expect_true(res$ok, info = res$msg)
})

test_that("CRP laplace: GPD, no X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("CRP laplace: GPD, no X", "laplace", "crp", TRUE, FALSE)
  expect_true(res$ok, info = res$msg)
})

test_that("CRP laplace: no GPD, with X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("CRP laplace: no GPD, with X", "laplace", "crp", FALSE, TRUE)
  expect_true(res$ok, info = res$msg)
})

test_that("CRP laplace: GPD, with X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("CRP laplace: GPD, with X", "laplace", "crp", TRUE, TRUE)
  expect_true(res$ok, info = res$msg)
})

# =============================================================================
# Lognormal kernel
# =============================================================================
test_that("SB lognormal: no GPD, no X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("SB lognormal: no GPD, no X", "lognormal", "sb", FALSE, FALSE)
  expect_true(res$ok, info = res$msg)
})

test_that("SB lognormal: GPD, no X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("SB lognormal: GPD, no X", "lognormal", "sb", TRUE, FALSE)
  expect_true(res$ok, info = res$msg)
})

test_that("SB lognormal: no GPD, with X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("SB lognormal: no GPD, with X", "lognormal", "sb", FALSE, TRUE)
  expect_true(res$ok, info = res$msg)
})

test_that("SB lognormal: GPD, with X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("SB lognormal: GPD, with X", "lognormal", "sb", TRUE, TRUE)
  expect_true(res$ok, info = res$msg)
})

test_that("CRP lognormal: no GPD, no X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("CRP lognormal: no GPD, no X", "lognormal", "crp", FALSE, FALSE)
  expect_true(res$ok, info = res$msg)
})

test_that("CRP lognormal: GPD, no X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("CRP lognormal: GPD, no X", "lognormal", "crp", TRUE, FALSE)
  expect_true(res$ok, info = res$msg)
})

test_that("CRP lognormal: no GPD, with X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("CRP lognormal: no GPD, with X", "lognormal", "crp", FALSE, TRUE)
  expect_true(res$ok, info = res$msg)
})

test_that("CRP lognormal: GPD, with X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("CRP lognormal: GPD, with X", "lognormal", "crp", TRUE, TRUE)
  expect_true(res$ok, info = res$msg)
})

# =============================================================================
# Normal kernel
# =============================================================================
test_that("SB normal: no GPD, no X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("SB normal: no GPD, no X", "normal", "sb", FALSE, FALSE)
  expect_true(res$ok, info = res$msg)
})

test_that("SB normal: GPD, no X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("SB normal: GPD, no X", "normal", "sb", TRUE, FALSE)
  expect_true(res$ok, info = res$msg)
})

test_that("SB normal: no GPD, with X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("SB normal: no GPD, with X", "normal", "sb", FALSE, TRUE)
  expect_true(res$ok, info = res$msg)
})

test_that("SB normal: GPD, with X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("SB normal: GPD, with X", "normal", "sb", TRUE, TRUE)
  expect_true(res$ok, info = res$msg)
})

test_that("CRP normal: no GPD, no X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("CRP normal: no GPD, no X", "normal", "crp", FALSE, FALSE)
  expect_true(res$ok, info = res$msg)
})

test_that("CRP normal: GPD, no X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("CRP normal: GPD, no X", "normal", "crp", TRUE, FALSE)
  expect_true(res$ok, info = res$msg)
})

test_that("CRP normal: no GPD, with X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("CRP normal: no GPD, with X", "normal", "crp", FALSE, TRUE)
  expect_true(res$ok, info = res$msg)
})

test_that("CRP normal: GPD, with X", {
  skip_if_not_test_level("ci")
  res <- .run_predict_case("CRP normal: GPD, with X", "normal", "crp", TRUE, TRUE)
  expect_true(res$ok, info = res$msg)
})

# ===== END unit/test-distributions.R =====

# ===== BEGIN unit/test-global-contracts.R =====
# Tests for global contract functions (00-global-contracts.R)

# ============================================================================
# is_allowed_kernel() tests
# ============================================================================

test_that("is_allowed_kernel() returns TRUE for valid kernels", {
  is_allowed_kernel <- getFromNamespace("is_allowed_kernel", "CausalMixGPD")

  expect_true(is_allowed_kernel("normal"))
  expect_true(is_allowed_kernel("gamma"))
  expect_true(is_allowed_kernel("lognormal"))
  expect_true(is_allowed_kernel("invgauss"))
  expect_true(is_allowed_kernel("laplace"))
  expect_true(is_allowed_kernel("cauchy"))
  expect_true(is_allowed_kernel("amoroso"))
})

test_that("is_allowed_kernel() returns FALSE for invalid kernels", {
  is_allowed_kernel <- getFromNamespace("is_allowed_kernel", "CausalMixGPD")

  expect_false(is_allowed_kernel("invalid"))
  expect_false(is_allowed_kernel("weibull"))
  expect_false(is_allowed_kernel("exponential"))
  expect_false(is_allowed_kernel(""))
  expect_false(is_allowed_kernel("Normal"))  # Case sensitive
})

# ============================================================================
# check_gpd_contract() tests
# ============================================================================

test_that("check_gpd_contract() errors when GPD=TRUE with cauchy kernel", {
  check_gpd_contract <- getFromNamespace("check_gpd_contract", "CausalMixGPD")

  expect_error(
    check_gpd_contract(GPD = TRUE, kernel = "cauchy"),
    "Cauchy kernels are never paired with GPD"
  )
})

test_that("check_gpd_contract() returns invisibly for valid combinations", {
  check_gpd_contract <- getFromNamespace("check_gpd_contract", "CausalMixGPD")

  # GPD = TRUE with non-cauchy kernels should work
  expect_invisible(check_gpd_contract(GPD = TRUE, kernel = "normal"))
  expect_invisible(check_gpd_contract(GPD = TRUE, kernel = "gamma"))
  expect_invisible(check_gpd_contract(GPD = TRUE, kernel = "lognormal"))
  expect_invisible(check_gpd_contract(GPD = TRUE, kernel = "invgauss"))
  expect_invisible(check_gpd_contract(GPD = TRUE, kernel = "laplace"))
  expect_invisible(check_gpd_contract(GPD = TRUE, kernel = "amoroso"))

  # GPD = FALSE should always work
  expect_invisible(check_gpd_contract(GPD = FALSE, kernel = "cauchy"))
  expect_invisible(check_gpd_contract(GPD = FALSE, kernel = "normal"))
})

test_that("check_gpd_contract() returns NULL invisibly", {
  check_gpd_contract <- getFromNamespace("check_gpd_contract", "CausalMixGPD")

  result <- check_gpd_contract(GPD = TRUE, kernel = "normal")
  expect_null(result)
})

# ============================================================================
# allowed_* constant tests
# ============================================================================

test_that("allowed_backends constant is correct", {
  allowed_backends <- getFromNamespace("allowed_backends", "CausalMixGPD")

  expect_equal(allowed_backends, c("crp", "sb", "spliced"))
})

test_that("allowed_kernels constant contains all 7 kernels", {
  allowed_kernels <- getFromNamespace("allowed_kernels", "CausalMixGPD")

  expected <- c("gamma", "lognormal", "invgauss", "normal", "laplace", "cauchy", "amoroso")
  expect_equal(sort(allowed_kernels), sort(expected))
})

test_that("positive_support_kernels constant is correct", {
  positive_support_kernels <- getFromNamespace("positive_support_kernels", "CausalMixGPD")

  expect_true("gamma" %in% positive_support_kernels)
  expect_true("lognormal" %in% positive_support_kernels)
  expect_true("invgauss" %in% positive_support_kernels)
  expect_true("amoroso" %in% positive_support_kernels)
  expect_false("normal" %in% positive_support_kernels)
  expect_false("laplace" %in% positive_support_kernels)
  expect_false("cauchy" %in% positive_support_kernels)
})

test_that("real_support_kernels constant is correct", {
  real_support_kernels <- getFromNamespace("real_support_kernels", "CausalMixGPD")

  expect_true("normal" %in% real_support_kernels)
  expect_true("laplace" %in% real_support_kernels)
  expect_true("cauchy" %in% real_support_kernels)
  expect_false("gamma" %in% real_support_kernels)
  expect_false("lognormal" %in% real_support_kernels)
  expect_false("invgauss" %in% real_support_kernels)
  expect_false("amoroso" %in% real_support_kernels)
})

# ===== END unit/test-global-contracts.R =====

# ===== BEGIN unit/test-glue-validity.R =====
# test-glue-validity.R
# CI-safe smoke test for glue diagnostics.

test_that("check_glue_validity passes on fixture (short grid, few draws)", {
  testthat::skip_on_cran()
  skip_if_not_test_level("ci")

  fit <- .load_fixture("fit_gpd_small.rds")
  small_grid <- seq(0.1, 5, length.out = 20)

  out <- check_glue_validity(fit, n_draws = 5L, grid = small_grid, check_continuity = FALSE)
  expect_true(is.list(out))
  expect_true(is.list(out$pass))
  expect_true(isTRUE(out$pass$cdf_range))
  expect_true(isTRUE(out$pass$cdf_monotone))
  expect_true(isTRUE(out$pass$density_nonneg))
})

# ===== END unit/test-glue-validity.R =====

# ===== BEGIN unit/test-hpd-intervals.R =====
# Test HPD interval functionality across all S3 functions
# Tests that interval = "hpd" works correctly and produces valid intervals

# ==============================================================================
# Test .compute_interval() helper function
# ==============================================================================

test_that(".compute_interval computes equal-tailed intervals correctly", {
  set.seed(42)
  draws <- rnorm(1000)

  iv <- .compute_interval(draws, level = 0.95, type = "credible")

  expect_named(iv, c("lower", "upper"))
  expect_true(iv["lower"] < iv["upper"])
  expect_true(iv["lower"] < 0)  # For standard normal
  expect_true(iv["upper"] > 0)

  # Check approximate quantiles
  q <- quantile(draws, probs = c(0.025, 0.975))
  expect_equal(unname(iv["lower"]), unname(q[1]))
  expect_equal(unname(iv["upper"]), unname(q[2]))
})


test_that(".compute_interval computes HPD intervals correctly", {
  set.seed(42)
  draws <- rnorm(1000)

  iv <- .compute_interval(draws, level = 0.95, type = "hpd")

  expect_named(iv, c("lower", "upper"))
  expect_true(iv["lower"] < iv["upper"])
  expect_true(iv["lower"] < 0)  # For standard normal
  expect_true(iv["upper"] > 0)

  # HPD should approximately match coda::HPDinterval
  hpd_coda <- coda::HPDinterval(coda::as.mcmc(draws), prob = 0.95)
  expect_equal(unname(iv["lower"]), unname(hpd_coda[1, "lower"]), tolerance = 0.001)
  expect_equal(unname(iv["upper"]), unname(hpd_coda[1, "upper"]), tolerance = 0.001)
})


test_that(".compute_interval handles skewed distributions differently", {
  set.seed(42)
  # Skewed distribution - HPD should give shorter interval
  draws <- rexp(1000)

  iv_credible <- .compute_interval(draws, level = 0.95, type = "credible")
  iv_hpd <- .compute_interval(draws, level = 0.95, type = "hpd")

  width_credible <- iv_credible["upper"] - iv_credible["lower"]
  width_hpd <- iv_hpd["upper"] - iv_hpd["lower"]

  # HPD interval should be narrower for skewed distributions
  expect_true(width_hpd <= width_credible + 0.01)
})


test_that(".compute_interval handles edge cases", {
  # Empty or NA draws
  iv <- .compute_interval(c(NA, NA, NA), level = 0.95, type = "credible")
  expect_true(is.na(iv["lower"]))
  expect_true(is.na(iv["upper"]))

  # Single value
  iv <- .compute_interval(c(1), level = 0.95, type = "credible")
  expect_true(is.na(iv["lower"]))
  expect_true(is.na(iv["upper"]))
})


# ==============================================================================
# Test predict.mixgpd_fit with interval parameter
# ==============================================================================

test_that("predict.mixgpd_fit supports unified interval parameter", {
  skip_if_not_test_level("ci")

  set.seed(42)
  y <- abs(rnorm(40)) + 0.2

  bundle <- build_nimble_bundle(
    y = y,
    backend = "sb",
    kernel = "gamma",
    GPD = FALSE,
    components = 6,
    mcmc = list(niter = 200, nburnin = 50, nchains = 1)
  )

  nullfile <- if (.Platform$OS.type == "windows") "NUL" else "/dev/null"
  utils::capture.output(
    fit <- run_mcmc_bundle_manual(bundle, show_progress = FALSE),
    file = nullfile
  )

  # Test quantile prediction with both interval types
  pred_credible <- predict(fit, type = "quantile", index = c(0.5, 0.9),
                           interval = "credible")
  pred_hpd <- predict(fit, type = "quantile", index = c(0.5, 0.9),
                      interval = "hpd")
  pred_none <- predict(fit, type = "quantile", index = c(0.5, 0.9),
                       interval = NULL)

  expect_s3_class(pred_credible, "mixgpd_predict")
  expect_s3_class(pred_hpd, "mixgpd_predict")
  expect_s3_class(pred_none, "mixgpd_predict")

  # Both credible and hpd should have valid intervals
  expect_true(all(pred_credible$fit$lower <= pred_credible$fit$estimate))
  expect_true(all(pred_credible$fit$estimate <= pred_credible$fit$upper))
  expect_true(all(pred_hpd$fit$lower <= pred_hpd$fit$estimate))
  expect_true(all(pred_hpd$fit$estimate <= pred_hpd$fit$upper))

  # NULL interval should have NA for lower/upper
  expect_true(all(is.na(pred_none$fit$lower)))
  expect_true(all(is.na(pred_none$fit$upper)))

  # Default should be credible
  pred_default <- predict(fit, type = "quantile", index = c(0.5, 0.9))
  expect_equal(pred_default$fit$lower, pred_credible$fit$lower)
  expect_equal(pred_default$fit$upper, pred_credible$fit$upper)
})


test_that("predict.mixgpd_fit mean prediction works with HPD", {
  skip_if_not_test_level("ci")

  set.seed(42)
  y <- abs(rnorm(40)) + 0.2

  bundle <- build_nimble_bundle(
    y = y,
    backend = "sb",
    kernel = "gamma",
    GPD = FALSE,
    components = 6,
    mcmc = list(niter = 200, nburnin = 50, nchains = 1)
  )

  nullfile <- if (.Platform$OS.type == "windows") "NUL" else "/dev/null"
  utils::capture.output(
    fit <- run_mcmc_bundle_manual(bundle, show_progress = FALSE),
    file = nullfile
  )

  pred_credible <- predict(fit, type = "mean", interval = "credible", nsim_mean = 100)
  pred_hpd <- predict(fit, type = "mean", interval = "hpd", nsim_mean = 100)

  expect_s3_class(pred_credible, "mixgpd_predict")
  expect_s3_class(pred_hpd, "mixgpd_predict")

  # Both should have valid intervals
  expect_true(pred_credible$fit$lower[1] <= pred_credible$fit$estimate[1])
  expect_true(pred_hpd$fit$lower[1] <= pred_hpd$fit$estimate[1])
})

test_that("bulk-only dpmix mean is analytical across kernels and cauchy rejects it", {
  init_kernel_registry()

  testthat::local_mocked_bindings(
    .validate_fit = function(object) TRUE,
    .cmgpd_progress_start = function(...) list(),
    .cmgpd_progress_step = function(...) invisible(NULL),
    .cmgpd_progress_done = function(...) invisible(NULL),
    .extract_draws_matrix = function(object) object$draw_mat,
    .package = "CausalMixGPD"
  )

  assemble_draw_mat <- function(weights, params = list(), betas = list(), scalar_draws = list()) {
    n_draws <- nrow(weights)
    cols <- list()
    for (k in seq_len(ncol(weights))) cols[[paste0("w[", k, "]")]] <- weights[, k]
    for (nm in names(params)) {
      mat <- params[[nm]]
      for (k in seq_len(ncol(mat))) cols[[paste0(nm, "[", k, "]")]] <- mat[, k]
    }
    for (nm in names(scalar_draws)) cols[[nm]] <- as.numeric(scalar_draws[[nm]])
    for (nm in names(betas)) {
      arr <- betas[[nm]]
      for (k in seq_len(dim(arr)[2])) {
        for (j in seq_len(dim(arr)[3])) {
          cols[[paste0("beta_", nm, "[", k, ",", j, "]")]] <- arr[, k, j]
        }
      }
    }
    out <- matrix(NA_real_, nrow = n_draws, ncol = length(cols))
    colnames(out) <- names(cols)
    for (j in seq_along(cols)) out[, j] <- cols[[j]]
    out
  }

  make_fit <- function(kernel, draw_mat, X = NULL, link_params = list(), GPD = FALSE, backend = "sb", gpd = list()) {
    structure(
      list(
        spec = list(
          meta = list(backend = backend, kernel = kernel, GPD = GPD, has_X = !is.null(X)),
          dispatch = list(backend = backend, GPD = GPD, link_params = link_params, gpd = gpd)
        ),
        data = list(
          X = X,
          y = rep(1, if (is.null(X)) 1L else nrow(X))
        ),
        draw_mat = draw_mat,
        epsilon = 1e-8
      ),
      class = "mixgpd_fit"
    )
  }

  weighted_mean <- function(w, component_mean) {
    sum((w / sum(w)) * component_mean)
  }

  kernel_component_mean <- function(kernel, pars) {
    switch(kernel,
      normal = pars$mean,
      lognormal = exp(pars$meanlog + 0.5 * pars$sdlog^2),
      gamma = pars$shape * pars$scale,
      laplace = pars$location,
      invgauss = pars$mean,
      amoroso = pars$loc + pars$scale * exp(lgamma(pars$shape1 + 1 / pars$shape2) - lgamma(pars$shape1))
    )
  }

  kernel_component_trunc_mean <- function(kernel, pars, threshold) {
    switch(kernel,
      normal = {
        a <- (threshold - pars$mean) / pars$sd
        pars$mean * pnorm(a) - pars$sd * dnorm(a)
      },
      lognormal = {
        if (threshold <= 0) {
          rep(0, length(pars$meanlog))
        } else {
          exp(pars$meanlog + 0.5 * pars$sdlog^2) *
            pnorm((log(threshold) - pars$meanlog - pars$sdlog^2) / pars$sdlog)
        }
      },
      gamma = {
        if (threshold <= 0) {
          rep(0, length(pars$shape))
        } else {
          pars$shape * pars$scale * pgamma(threshold, shape = pars$shape + 1, scale = pars$scale)
        }
      },
      laplace = ifelse(
        threshold < pars$location,
        0.5 * (threshold - pars$scale) * exp((threshold - pars$location) / pars$scale),
        pars$location - 0.5 * (threshold + pars$scale) * exp(-(threshold - pars$location) / pars$scale)
      ),
      invgauss = {
        if (threshold <= 0) {
          rep(0, length(pars$mean))
        } else {
          sqrt_term <- sqrt(pars$shape / threshold)
          a <- sqrt_term * (threshold / pars$mean - 1)
          b <- -sqrt_term * (threshold / pars$mean + 1)
          pars$mean * (pnorm(a) - exp(2 * pars$shape / pars$mean) * pnorm(b))
        }
      },
      amoroso = {
        z <- pmax((threshold - pars$loc) / pars$scale, 0)^pars$shape2
        pars$loc * pgamma(z, shape = pars$shape1, scale = 1) +
          pars$scale * exp(lgamma(pars$shape1 + 1 / pars$shape2) - lgamma(pars$shape1)) *
          pgamma(z, shape = pars$shape1 + 1 / pars$shape2, scale = 1)
      }
    )
  }

  kernel_spliced_mean <- function(kernel, w, pars, threshold, tail_scale, tail_shape) {
    trunc_mean <- kernel_component_trunc_mean(kernel, pars, threshold)
    Fu <- switch(kernel,
      normal = pnormmix(threshold, w = w, mean = pars$mean, sd = pars$sd),
      lognormal = plognormalmix(threshold, w = w, meanlog = pars$meanlog, sdlog = pars$sdlog),
      gamma = pgammamix(threshold, w = w, shape = pars$shape, scale = pars$scale),
      laplace = plaplacemix(threshold, w = w, location = pars$location, scale = pars$scale),
      invgauss = pinvgaussmix(threshold, w = w, mean = pars$mean, shape = pars$shape),
      amoroso = pamorosomix(threshold, w = w, loc = pars$loc, scale = pars$scale, shape1 = pars$shape1, shape2 = pars$shape2)
    )
    weighted_mean(w, trunc_mean) + (1 - Fu) * (threshold + tail_scale / (1 - tail_shape))
  }

  unconditional_cases <- list(
    normal = list(
      weights = rbind(c(0.7, 0.3), c(0.2, 0.8)),
      params = list(mean = rbind(c(1, 3), c(2, 4)), sd = rbind(c(1.1, 1.4), c(1.2, 1.5)))
    ),
    lognormal = list(
      weights = rbind(c(0.6, 0.4), c(0.3, 0.7)),
      params = list(meanlog = rbind(c(0.1, 0.4), c(0.2, 0.5)), sdlog = rbind(c(0.3, 0.2), c(0.25, 0.35)))
    ),
    gamma = list(
      weights = rbind(c(0.5, 0.5), c(0.25, 0.75)),
      params = list(shape = rbind(c(2, 4), c(3, 5)), scale = rbind(c(1.0, 0.8), c(0.9, 1.1)))
    ),
    laplace = list(
      weights = rbind(c(0.65, 0.35), c(0.45, 0.55)),
      params = list(location = rbind(c(-1, 2), c(0.5, 1.5)), scale = rbind(c(1.2, 0.7), c(1.1, 0.9)))
    ),
    invgauss = list(
      weights = rbind(c(0.55, 0.45), c(0.4, 0.6)),
      params = list(mean = rbind(c(1.5, 2.5), c(2.0, 3.5)), shape = rbind(c(4, 6), c(5, 7)))
    ),
    amoroso = list(
      weights = rbind(c(0.6, 0.4), c(0.35, 0.65)),
      params = list(
        loc = rbind(c(0.2, 0.5), c(0.3, 0.6)),
        scale = rbind(c(1.1, 0.9), c(1.2, 1.0)),
        shape1 = rbind(c(2.5, 3.0), c(2.2, 2.8)),
        shape2 = rbind(c(1.4, 1.6), c(1.5, 1.8))
      )
    )
  )

  for (kernel in names(unconditional_cases)) {
    case <- unconditional_cases[[kernel]]
    draw_mat <- assemble_draw_mat(case$weights, params = case$params)
    fit <- make_fit(kernel, draw_mat = draw_mat)
    pred <- .predict_mixgpd(fit, type = "mean", store_draws = TRUE, show_progress = FALSE)
    expected_draws <- vapply(seq_len(nrow(case$weights)), function(s) {
      pars <- lapply(case$params, function(mat) mat[s, ])
      weighted_mean(case$weights[s, ], kernel_component_mean(kernel, pars))
    }, numeric(1))
    expect_equal(as.numeric(pred$draws), expected_draws, tolerance = 1e-10, info = kernel)
    expect_equal(pred$fit$estimate, mean(expected_draws), tolerance = 1e-10, info = kernel)
    expect_equal(pred$diagnostics$mean_method, "analytic", info = kernel)
    expect_null(pred$diagnostics$nsim_mean, info = kernel)
  }

  Xpred <- matrix(c(1, 2), ncol = 1, dimnames = list(NULL, "x1"))
  conditional_cases <- list(
    normal = list(
      weights = rbind(c(0.7, 0.3), c(0.2, 0.8)),
      params = list(sd = rbind(c(1.1, 1.4), c(1.2, 1.5))),
      betas = list(mean = array(c(1, 2, 0, 3), dim = c(2, 2, 1))),
      link_params = list(mean = list(mode = "link", link = "identity"))
    ),
    lognormal = list(
      weights = rbind(c(0.6, 0.4), c(0.3, 0.7)),
      params = list(sdlog = rbind(c(0.3, 0.2), c(0.25, 0.35))),
      betas = list(meanlog = array(c(0.1, 0.3, 0.2, 0.4), dim = c(2, 2, 1))),
      link_params = list(meanlog = list(mode = "link", link = "identity"))
    ),
    gamma = list(
      weights = rbind(c(0.5, 0.5), c(0.25, 0.75)),
      params = list(shape = rbind(c(2, 4), c(3, 5))),
      betas = list(scale = array(c(log(1.0), log(0.6), log(0.8), log(0.5)), dim = c(2, 2, 1))),
      link_params = list(scale = list(mode = "link", link = "exp"))
    ),
    laplace = list(
      weights = rbind(c(0.65, 0.35), c(0.45, 0.55)),
      params = list(scale = rbind(c(1.2, 0.7), c(1.1, 0.9))),
      betas = list(location = array(c(-0.5, 0.8, 0.25, 1.0), dim = c(2, 2, 1))),
      link_params = list(location = list(mode = "link", link = "identity"))
    ),
    invgauss = list(
      weights = rbind(c(0.55, 0.45), c(0.4, 0.6)),
      params = list(shape = rbind(c(4, 6), c(5, 7))),
      betas = list(mean = array(c(log(1.2), log(2.0), log(1.5), log(2.2)), dim = c(2, 2, 1))),
      link_params = list(mean = list(mode = "link", link = "exp"))
    ),
    amoroso = list(
      weights = rbind(c(0.6, 0.4), c(0.35, 0.65)),
      params = list(
        scale = rbind(c(1.1, 0.9), c(1.2, 1.0)),
        shape1 = rbind(c(2.5, 3.0), c(2.2, 2.8)),
        shape2 = rbind(c(1.4, 1.6), c(1.5, 1.8))
      ),
      betas = list(loc = array(c(0.2, 0.4, 0.3, 0.5), dim = c(2, 2, 1))),
      link_params = list(loc = list(mode = "link", link = "identity"))
    )
  )

  apply_link <- function(x, link) {
    if (identical(link, "exp")) return(exp(x))
    x
  }

  for (kernel in names(conditional_cases)) {
    case <- conditional_cases[[kernel]]
    draw_mat <- assemble_draw_mat(case$weights, params = case$params, betas = case$betas)
    fit <- make_fit(kernel, draw_mat = draw_mat, X = Xpred, link_params = case$link_params)
    pred <- .predict_mixgpd(fit, x = Xpred, type = "mean", store_draws = TRUE, show_progress = FALSE)
    expected_draws <- matrix(NA_real_, nrow = nrow(case$weights), ncol = nrow(Xpred))
    link_name <- names(case$link_params)[1]
    link_fun <- case$link_params[[1]]$link
    beta_arr <- case$betas[[link_name]]
    for (s in seq_len(nrow(case$weights))) {
      for (i in seq_len(nrow(Xpred))) {
        pars <- lapply(case$params, function(mat) mat[s, ])
        pars[[link_name]] <- apply_link(Xpred[i, 1] * beta_arr[s, , 1], link_fun)
        expected_draws[s, i] <- weighted_mean(case$weights[s, ], kernel_component_mean(kernel, pars))
      }
    }
    expect_equal(pred$draws, expected_draws, tolerance = 1e-10, info = kernel)
    expect_equal(pred$fit$estimate, colMeans(expected_draws), tolerance = 1e-10, info = kernel)
    expect_equal(pred$diagnostics$mean_method, "analytic", info = kernel)
  }

  gpd_cases <- list(
    normal = list(
      weights = rbind(c(0.7, 0.3), c(0.2, 0.8)),
      params = list(mean = rbind(c(1, 3), c(2, 4)), sd = rbind(c(1.1, 1.4), c(1.2, 1.5))),
      threshold = c(2.5, 3.0),
      tail_scale = c(0.8, 0.6),
      tail_shape = c(0.2, 0.4)
    ),
    lognormal = list(
      weights = rbind(c(0.6, 0.4), c(0.3, 0.7)),
      params = list(meanlog = rbind(c(0.1, 0.4), c(0.2, 0.5)), sdlog = rbind(c(0.3, 0.2), c(0.25, 0.35))),
      threshold = c(1.8, 2.4),
      tail_scale = c(0.7, 0.5),
      tail_shape = c(0.25, 0.3)
    ),
    gamma = list(
      weights = rbind(c(0.5, 0.5), c(0.25, 0.75)),
      params = list(shape = rbind(c(2, 4), c(3, 5)), scale = rbind(c(1.0, 0.8), c(0.9, 1.1))),
      threshold = c(3.0, 4.2),
      tail_scale = c(0.9, 0.7),
      tail_shape = c(0.15, 0.35)
    ),
    laplace = list(
      weights = rbind(c(0.65, 0.35), c(0.45, 0.55)),
      params = list(location = rbind(c(-1, 2), c(0.5, 1.5)), scale = rbind(c(1.2, 0.7), c(1.1, 0.9))),
      threshold = c(1.2, 2.1),
      tail_scale = c(0.75, 0.55),
      tail_shape = c(0.1, 0.2)
    ),
    invgauss = list(
      weights = rbind(c(0.55, 0.45), c(0.4, 0.6)),
      params = list(mean = rbind(c(1.5, 2.5), c(2.0, 3.5)), shape = rbind(c(4, 6), c(5, 7))),
      threshold = c(2.5, 3.2),
      tail_scale = c(0.65, 0.8),
      tail_shape = c(0.2, 0.45)
    ),
    amoroso = list(
      weights = rbind(c(0.6, 0.4), c(0.35, 0.65)),
      params = list(
        loc = rbind(c(0.2, 0.5), c(0.3, 0.6)),
        scale = rbind(c(1.1, 0.9), c(1.2, 1.0)),
        shape1 = rbind(c(2.5, 3.0), c(2.2, 2.8)),
        shape2 = rbind(c(1.4, 1.6), c(1.5, 1.8))
      ),
      threshold = c(2.1, 2.4),
      tail_scale = c(0.7, 0.6),
      tail_shape = c(0.2, 0.3)
    )
  )

  for (kernel in names(gpd_cases)) {
    case <- gpd_cases[[kernel]]
    draw_mat <- assemble_draw_mat(
      case$weights,
      params = case$params,
      scalar_draws = list(
        threshold = case$threshold,
        tail_scale = case$tail_scale,
        tail_shape = case$tail_shape
      )
    )
    fit <- make_fit(
      kernel,
      draw_mat = draw_mat,
      GPD = TRUE,
      gpd = list(
        threshold = list(mode = "constant"),
        tail_scale = list(mode = "constant"),
        tail_shape = list(mode = "constant")
      )
    )
    pred <- .predict_mixgpd(fit, type = "mean", store_draws = TRUE, show_progress = FALSE)
    expected_draws <- vapply(seq_len(nrow(case$weights)), function(s) {
      pars <- lapply(case$params, function(mat) mat[s, ])
      kernel_spliced_mean(
        kernel,
        w = case$weights[s, ],
        pars = pars,
        threshold = case$threshold[s],
        tail_scale = case$tail_scale[s],
        tail_shape = case$tail_shape[s]
      )
    }, numeric(1))
    expect_equal(as.numeric(pred$draws), expected_draws, tolerance = 1e-8, info = kernel)
    expect_equal(pred$fit$estimate, mean(expected_draws), tolerance = 1e-8, info = kernel)
    expect_equal(pred$diagnostics$mean_method, "analytic", info = kernel)
    expect_null(pred$diagnostics$nsim_mean, info = kernel)
  }

  gpd_cond_draw_mat <- assemble_draw_mat(
    weights = rbind(c(0.7, 0.3), c(0.2, 0.8)),
    params = list(sd = rbind(c(1.1, 1.4), c(1.2, 1.5))),
    betas = list(mean = array(c(1, 2, 0, 3), dim = c(2, 2, 1))),
    scalar_draws = list(
      threshold = c(2.5, 3.0),
      tail_scale = c(0.8, 0.6),
      tail_shape = c(0.2, 0.4)
    )
  )
  gpd_cond_fit <- make_fit(
    "normal",
    draw_mat = gpd_cond_draw_mat,
    X = Xpred,
    link_params = list(mean = list(mode = "link", link = "identity")),
    GPD = TRUE,
    gpd = list(
      threshold = list(mode = "constant"),
      tail_scale = list(mode = "constant"),
      tail_shape = list(mode = "constant")
    )
  )
  gpd_cond_pred <- .predict_mixgpd(gpd_cond_fit, x = Xpred, type = "mean", store_draws = TRUE, show_progress = FALSE)
  gpd_cond_expected <- matrix(NA_real_, nrow = 2, ncol = nrow(Xpred))
  beta_arr <- array(c(1, 2, 0, 3), dim = c(2, 2, 1))
  for (s in 1:2) {
    for (i in seq_len(nrow(Xpred))) {
      pars <- list(
        mean = Xpred[i, 1] * beta_arr[s, , 1],
        sd = c(1.1, 1.4)
      )
      if (s == 2) pars$sd <- c(1.2, 1.5)
      gpd_cond_expected[s, i] <- kernel_spliced_mean(
        "normal",
        w = if (s == 1) c(0.7, 0.3) else c(0.2, 0.8),
        pars = pars,
        threshold = if (s == 1) 2.5 else 3.0,
        tail_scale = if (s == 1) 0.8 else 0.6,
        tail_shape = if (s == 1) 0.2 else 0.4
      )
    }
  }
  expect_equal(gpd_cond_pred$draws, gpd_cond_expected, tolerance = 1e-8)
  expect_equal(gpd_cond_pred$fit$estimate, colMeans(gpd_cond_expected), tolerance = 1e-8)
  expect_equal(gpd_cond_pred$diagnostics$mean_method, "analytic")

  xi_bad_draw_mat <- assemble_draw_mat(
    weights = rbind(c(0.7, 0.3), c(0.2, 0.8)),
    params = list(mean = rbind(c(1, 3), c(2, 4)), sd = rbind(c(1.1, 1.4), c(1.2, 1.5))),
    scalar_draws = list(threshold = c(2.5, 3.0), tail_scale = c(0.8, 0.6), tail_shape = c(1.1, 0.4))
  )
  xi_bad_fit <- make_fit(
    "normal",
    draw_mat = xi_bad_draw_mat,
    GPD = TRUE,
    gpd = list(
      threshold = list(mode = "constant"),
      tail_scale = list(mode = "constant"),
      tail_shape = list(mode = "constant")
    )
  )
  expect_error(
    .predict_mixgpd(xi_bad_fit, type = "mean", show_progress = FALSE),
    "tail_shape \\(xi\\) >= 1; use type='rmean'"
  )

  cauchy_draw_mat <- assemble_draw_mat(
    weights = rbind(c(0.6, 0.4), c(0.5, 0.5)),
    params = list(location = rbind(c(0, 1), c(-1, 2)), scale = rbind(c(1, 2), c(1.5, 0.75)))
  )
  cauchy_fit <- make_fit("cauchy", draw_mat = cauchy_draw_mat)
  expect_error(
    .predict_mixgpd(cauchy_fit, type = "mean", show_progress = FALSE),
    "Mean is not supported for the Cauchy kernel; use type='rmean'"
  )
})


# ==============================================================================
# Test fitted.mixgpd_fit with interval parameter
# ==============================================================================

test_that("fitted.mixgpd_fit supports unified interval parameter", {
  skip_if_not_test_level("ci")

  set.seed(42)
  n <- 30
  y <- abs(rnorm(n)) + 0.2
  X <- data.frame(x1 = rnorm(n), x2 = runif(n))

  bundle <- build_nimble_bundle(
    y = y,
    X = X,
    backend = "sb",
    kernel = "gamma",
    GPD = FALSE,
    components = 6,
    mcmc = list(niter = 200, nburnin = 50, nchains = 1)
  )

  nullfile <- if (.Platform$OS.type == "windows") "NUL" else "/dev/null"
  utils::capture.output(
    fit <- run_mcmc_bundle_manual(bundle, show_progress = FALSE),
    file = nullfile
  )

  fitted_credible <- fitted(fit, type = "mean", interval = "credible")
  fitted_hpd <- fitted(fit, type = "mean", interval = "hpd")
  fitted_none <- fitted(fit, type = "mean", interval = NULL)

  expect_s3_class(fitted_credible, "mixgpd_fitted")
  expect_s3_class(fitted_hpd, "mixgpd_fitted")
  expect_s3_class(fitted_none, "mixgpd_fitted")

  # Both should have valid structure
  expect_true(all(c("fit", "lower", "upper", "residuals") %in% names(fitted_credible)))
  expect_true(all(c("fit", "lower", "upper", "residuals") %in% names(fitted_hpd)))
  expect_true(all(c("fit", "lower", "upper", "residuals") %in% names(fitted_none)))

  # Check interval is stored
  expect_equal(attr(fitted_credible, "interval"), "credible")
  expect_equal(attr(fitted_hpd, "interval"), "hpd")
  expect_null(attr(fitted_none, "interval"))
})


# ==============================================================================
# Test .posterior_summarize with interval parameter
# ==============================================================================

test_that(".posterior_summarize uses interval correctly", {
  set.seed(42)
  draws <- matrix(rexp(500), nrow = 5, ncol = 100)

  summ_credible <- .posterior_summarize(draws, interval = "credible")
  summ_hpd <- .posterior_summarize(draws, interval = "hpd")
  summ_none <- .posterior_summarize(draws, interval = NULL)

  # Check structure
  expect_named(summ_credible, c("estimate", "lower", "upper", "q"))
  expect_named(summ_hpd, c("estimate", "lower", "upper", "q"))
  expect_named(summ_none, c("estimate", "lower", "upper", "q"))

  # Estimates should be the same (all use mean)
  expect_equal(summ_credible$estimate, summ_hpd$estimate)
  expect_equal(summ_credible$estimate, summ_none$estimate)

  # Intervals should be different for skewed distribution (credible vs hpd)
  expect_false(all(summ_credible$lower == summ_hpd$lower))
  expect_false(all(summ_credible$upper == summ_hpd$upper))

  # NULL interval should give NA for lower/upper
  expect_true(all(is.na(summ_none$lower)))
  expect_true(all(is.na(summ_none$upper)))
})

# ===== END unit/test-hpd-intervals.R =====

# ===== BEGIN unit/test-internal-utils.R =====
# tests/testthat/test-internal-utils.R
# Unit tests for internal utility helpers (02-utilities-internal.R)

# ======================================================================
# .validate_nimble_reserved_names
# ======================================================================

test_that(".validate_nimble_reserved_names accepts valid names", {
  expect_invisible(.validate_nimble_reserved_names(c("alpha", "beta", "sigma")))
  expect_invisible(.validate_nimble_reserved_names(character(0)))
  expect_invisible(.validate_nimble_reserved_names(NULL))
  expect_invisible(.validate_nimble_reserved_names(c("x_if", "myfor")))
})

test_that(".validate_nimble_reserved_names rejects reserved keywords", {
  expect_error(
    .validate_nimble_reserved_names(c("if", "alpha"), context = "test"),
    regexp = "reserved NIMBLE keywords"
  )
  expect_error(
    .validate_nimble_reserved_names(c("FOR", "WHILE")),
    regexp = "reserved NIMBLE keywords"
  )
  expect_error(
    .validate_nimble_reserved_names(c("T", "F")),
    regexp = "reserved NIMBLE keywords"
  )
  expect_error(
    .validate_nimble_reserved_names(c("na", "nan", "inf")),
    regexp = "reserved NIMBLE keywords"
  )
})

test_that(".validate_nimble_reserved_names handles edge cases", {
  expect_invisible(.validate_nimble_reserved_names(c("", NA_character_)))
  expect_invisible(.validate_nimble_reserved_names(c("iffy", "truly", "nullify")))
})

# ======================================================================
# Plot helpers / code wrappers
# ======================================================================

test_that(".plot_palette returns requested length", {
  expect_length(.plot_palette(3), 3)
  expect_length(.plot_palette(8), 8)
  expect_length(.plot_palette(12), 12)
})

test_that(".extract_nimble_code and .wrap_nimble_code handle wrapped/list code", {
  code_obj <- quote({ a <- 1 })
  wrapped <- .wrap_nimble_code(code_obj)
  expect_true(is.list(wrapped))
  expect_true(!is.null(wrapped$nimble))

  # extract returns $nimble when stored in a list wrapper
  extracted <- .extract_nimble_code(wrapped)
  expect_equal(deparse(extracted), deparse(code_obj))

  # if already a list, wrapper should pass-through
  pass <- .wrap_nimble_code(list(nimble = code_obj))
  expect_true(is.list(pass))
  expect_true(!is.null(pass$nimble))
})

test_that(".wrap_plotly returns plotly when available, otherwise passthrough", {
  p <- ggplot2::ggplot() + ggplot2::geom_point(ggplot2::aes(1, 1))

  out <- .wrap_plotly(p)
  if (requireNamespace("plotly", quietly = TRUE)) {
    old_opt <- getOption("CausalMixGPD.plotly")
    options(CausalMixGPD.plotly = TRUE)
    on.exit(options(CausalMixGPD.plotly = old_opt), add = TRUE)
    out <- .wrap_plotly(p)
    expect_true(is.list(out))
    expect_true(inherits(out, "plotly") || inherits(out, "htmlwidget"))
  } else {
    expect_true(inherits(out, "ggplot"))
  }

  plots <- list(a = p, b = p)
  class(plots) <- c("mixgpd_fit_plots", "list")
  out2 <- .wrap_plotly(plots)
  expect_true(inherits(out2, "mixgpd_fit_plots"))
  expect_true(is.list(out2))
})

# ======================================================================
# .coerce_fit_df
# ======================================================================

test_that(".coerce_fit_df handles data.frame input", {
  df <- data.frame(estimate = 1:3, lower = 0:2, upper = 2:4)
  result <- .coerce_fit_df(df)
  expect_s3_class(result, "data.frame")
  expect_true(all(c("estimate", "lower", "upper", "id") %in% names(result)))
  expect_equal(result$estimate, 1:3)
})

test_that(".coerce_fit_df handles data.frame missing estimate", {
  df <- data.frame(fit = c(1, 2), other = c(3, 4))
  result <- .coerce_fit_df(df)
  expect_equal(result$estimate, c(1, 2))
  expect_true("id" %in% names(result))
})

test_that(".coerce_fit_df handles data.frame missing fit/estimate column", {
  df <- data.frame(a = 1:3, b = 4:6)
  result <- .coerce_fit_df(df)
  expect_equal(result$estimate, 1:3)  # uses first numeric column
})

test_that(".coerce_fit_df handles numeric matrix with named columns", {
  mat <- matrix(1:9, nrow = 3, ncol = 3)
  colnames(mat) <- c("estimate", "lower", "upper")
  result <- .coerce_fit_df(mat)
  expect_s3_class(result, "data.frame")
  expect_true("id" %in% names(result))
  expect_equal(nrow(result), 3)
})

test_that(".coerce_fit_df handles matrix with probs expansion", {
  mat <- matrix(1:6, nrow = 2, ncol = 3)
  probs <- c(0.25, 0.5, 0.75)
  result <- .coerce_fit_df(mat, n_pred = 2, probs = probs)
  expect_s3_class(result, "data.frame")
  expect_true(all(c("id", "index", "estimate") %in% names(result)))
  expect_equal(nrow(result), 6)  # 2 rows x 3 probs
})

test_that(".coerce_fit_df handles matrix default (first column)", {

  mat <- matrix(1:6, nrow = 3, ncol = 2)
  result <- .coerce_fit_df(mat)
  expect_equal(result$estimate, 1:3)
  expect_equal(result$lower, 4:6)
})

test_that(".coerce_fit_df handles numeric vector", {
  vec <- c(1.0, 2.0, 3.0)
  result <- .coerce_fit_df(vec)
  expect_s3_class(result, "data.frame")
  expect_equal(result$estimate, vec)
  expect_equal(result$id, 1:3)
})

test_that(".coerce_fit_df errors on unsupported type", {
  expect_error(.coerce_fit_df(list(a = 1)), regexp = "Cannot coerce")
})

# ======================================================================
# .compute_interval
# ======================================================================

test_that(".compute_interval credible returns expected quantiles", {
  draws <- rnorm(1000, mean = 5, sd = 1)
  iv <- .compute_interval(draws, level = 0.95, type = "credible")
  expect_named(iv, c("lower", "upper"))
  expect_true(iv["lower"] < 5)

  expect_true(iv["upper"] > 5)
  expect_true(iv["lower"] < iv["upper"])
})

test_that(".compute_interval hpd returns valid interval", {
  skip_if_not_installed("coda")
  draws <- rnorm(1000, mean = 0, sd = 1)
  iv <- .compute_interval(draws, level = 0.95, type = "hpd")
  expect_named(iv, c("lower", "upper"))
  expect_true(iv["lower"] < iv["upper"])
})
test_that(".compute_interval handles insufficient draws", {
  expect_equal(.compute_interval(numeric(0)), c(lower = NA_real_, upper = NA_real_))
  expect_equal(.compute_interval(c(1)), c(lower = NA_real_, upper = NA_real_))
})

test_that(".compute_interval filters non-finite", {
  draws <- c(1, 2, 3, NA, Inf, -Inf, 4, 5)
  iv <- .compute_interval(draws, level = 0.95, type = "credible")
  expect_true(is.finite(iv["lower"]))
  expect_true(is.finite(iv["upper"]))
})

# ======================================================================
# .posterior_summarize
# ======================================================================

test_that(".posterior_summarize works on numeric vector", {
  draws <- rnorm(100, mean = 10, sd = 2)
  result <- .posterior_summarize(draws, probs = c(0.025, 0.5, 0.975), interval = "credible")
  expect_named(result, c("estimate", "lower", "upper", "q"))
  expect_length(result$estimate, 1)
  expect_true(result$lower < result$upper)
})

test_that(".posterior_summarize works on 2D matrix", {
  draws <- matrix(rnorm(300), nrow = 3, ncol = 100)
  result <- .posterior_summarize(draws, probs = c(0.025, 0.975), interval = "credible")
  expect_length(result$estimate, 3)
  expect_length(result$lower, 3)
  expect_length(result$upper, 3)
})

test_that(".posterior_summarize works on 3D array", {
  draws <- array(rnorm(600), dim = c(2, 3, 100))
  result <- .posterior_summarize(draws, probs = c(0.025, 0.975), interval = "credible")
  expect_equal(dim(result$estimate), c(2, 3))
  expect_equal(dim(result$lower), c(2, 3))
  expect_equal(dim(result$upper), c(2, 3))
})

test_that(".posterior_summarize handles interval = NULL", {
  draws <- rnorm(100)
  result <- .posterior_summarize(draws, probs = c(0.025, 0.975), interval = NULL)
  expect_true(is.na(result$lower))
  expect_true(is.na(result$upper))
})

test_that(".posterior_summarize hpd mode", {
  skip_if_not_installed("coda")
  draws <- rnorm(200)
  result <- .posterior_summarize(draws, probs = c(0.025, 0.975), interval = "hpd")
  expect_true(!is.na(result$lower))
  expect_true(!is.na(result$upper))
})

# ======================================================================
# .truncate_components_one_draw
# ======================================================================

test_that(".truncate_components_one_draw sorts by weight", {
  w <- c(0.1, 0.6, 0.3)
  params <- list(mu = c(1, 2, 3), sigma = c(0.5, 1, 1.5))
  result <- .truncate_components_one_draw(w, params, epsilon = 0.01)

  expect_equal(result$ord, c(2, 3, 1))  # sorted descending by weight
  expect_true(sum(result$weights) > 0.99)
})

test_that(".truncate_components_one_draw truncates small components", {
  w <- c(0.05, 0.9, 0.05)
  params <- list(mu = c(1, 2, 3))
  result <- .truncate_components_one_draw(w, params, epsilon = 0.1)

  expect_true(result$k <= 3)
  expect_true(length(result$weights) <= 3)
})

test_that(".truncate_components_one_draw errors on invalid epsilon", {
  w <- c(0.5, 0.5)
  params <- list(mu = c(1, 2))

  expect_error(.truncate_components_one_draw(w, params, epsilon = -0.1))
  expect_error(.truncate_components_one_draw(w, params, epsilon = 1.0))
  expect_error(.truncate_components_one_draw(w, params, epsilon = NA))
  expect_error(.truncate_components_one_draw(w, params, epsilon = c(0.1, 0.2)))
})

test_that(".truncate_components_one_draw handles empty params", {
  w <- c(0.3, 0.7)
  result <- .truncate_components_one_draw(w, params = list(), epsilon = 0.01)
  expect_true(result$k >= 1)
})

test_that(".truncate_components_one_draw validates params length", {
  w <- c(0.5, 0.5)
  params <- list(mu = c(1, 2, 3))  # wrong length

  expect_error(.truncate_components_one_draw(w, params, epsilon = 0.1))
})

test_that(".truncate_components_one_draw adjusts weights to sum to 1", {
  w <- c(0.6, 0.3, 0.1)
  params <- list(mu = c(1, 2, 3))
  result <- .truncate_components_one_draw(w, params, epsilon = 0.05)

  expect_equal(sum(result$weights), 1, tolerance = 1e-10)
})

# ======================================================================
# Additional internal utility helpers
# ======================================================================

# ======================================================================
# .extract_nimble_code tests
# ======================================================================

test_that(".extract_nimble_code extracts from list with nimble", {
  code_obj <- list(nimble = quote(y ~ dnorm(0, 1)))
  result <- .extract_nimble_code(code_obj)
  expect_equal(result, quote(y ~ dnorm(0, 1)))
})

test_that(".extract_nimble_code extracts from list with code", {
  code_obj <- list(code = quote(y ~ dnorm(0, 1)))
  result <- .extract_nimble_code(code_obj)
  expect_equal(result, quote(y ~ dnorm(0, 1)))
})

test_that(".extract_nimble_code returns nimbleCode as-is", {
  code <- quote(y ~ dnorm(0, 1))
  class(code) <- c("nimbleCode", class(code))
  result <- .extract_nimble_code(code)
  expect_equal(result, code)
})

test_that(".extract_nimble_code returns non-list as-is", {
  result <- .extract_nimble_code("some_code")
  expect_equal(result, "some_code")
})

# ======================================================================
# .wrap_nimble_code tests
# ======================================================================

test_that(".wrap_nimble_code wraps non-list code", {
  code <- quote(y ~ dnorm(0, 1))
  result <- .wrap_nimble_code(code)
  expect_true(is.list(result))
  expect_equal(result$nimble, code)
})

test_that(".wrap_nimble_code returns list as-is", {
  code_list <- list(nimble = quote(y ~ dnorm(0, 1)))
  result <- .wrap_nimble_code(code_list)
  expect_equal(result, code_list)
})

# ======================================================================
# .backend_label tests
# ======================================================================

test_that(".backend_label returns correct labels", {
  expect_equal(.backend_label("sb"), "Stick-Breaking Process")
  expect_equal(.backend_label("crp"), "Chinese Restaurant Process")
  expect_equal(.backend_label("unknown"), "unknown")
})

# ======================================================================
# .kernel_label tests
# ======================================================================

test_that(".kernel_label returns correct labels", {
  expect_equal(.kernel_label("normal"), "Normal Distribution")
  expect_equal(.kernel_label("gamma"), "Gamma Distribution")
  expect_equal(.kernel_label("lognormal"), "Lognormal Distribution")
  expect_equal(.kernel_label("laplace"), "Laplace Distribution")
  expect_equal(.kernel_label("invgauss"), "Inverse Gaussian Distribution")
  expect_equal(.kernel_label("amoroso"), "Amoroso Distribution")
  expect_equal(.kernel_label("cauchy"), "Cauchy Distribution")
  expect_equal(.kernel_label("unknown"), "unknown")
})

# ======================================================================
# .plot_palette tests
# ======================================================================

test_that(".plot_palette returns valid hex colors", {
  pal <- .plot_palette(4)
  expect_length(pal, 4)
  expect_true(all(grepl("^#[0-9A-Fa-f]{6}$", pal)))
})

test_that(".plot_palette recycles for n > 8", {
  pal <- .plot_palette(10)
  expect_length(pal, 10)
})

test_that(".plot_palette handles NULL", {
  pal <- .plot_palette(NULL)
  expect_length(pal, 8)
})

# ======================================================================
# .get_epsilon tests
# ======================================================================

test_that(".get_epsilon returns provided epsilon", {
  obj <- list(spec = list(meta = list(epsilon = 0.05)))
  result <- .get_epsilon(obj, epsilon = 0.1)
  expect_equal(result, 0.1)
})

test_that(".get_epsilon extracts from spec$meta", {
  obj <- list(spec = list(meta = list(epsilon = 0.05)))
  result <- .get_epsilon(obj, epsilon = NULL)
  expect_equal(result, 0.05)
})

test_that(".get_epsilon extracts from object$epsilon", {
  obj <- list(epsilon = 0.03)
  result <- .get_epsilon(obj, epsilon = NULL)
  expect_equal(result, 0.03)
})

test_that(".get_epsilon returns default 0.025", {
  obj <- list()
  result <- .get_epsilon(obj, epsilon = NULL)
  expect_equal(result, 0.025)
})

# ======================================================================
# .get_nobs tests
# ======================================================================

test_that(".get_nobs extracts from data$y", {
  obj <- list(data = list(y = 1:100))
  result <- .get_nobs(obj)
  expect_equal(result, 100)
})

test_that(".get_nobs extracts from y directly", {
  obj <- list(y = 1:50)
  result <- .get_nobs(obj)
  expect_equal(result, 50)
})

test_that(".get_nobs returns NA when not found", {
  obj <- list()
  result <- .get_nobs(obj)
  expect_true(is.na(result))
})

# ===== END unit/test-internal-utils.R =====

# ===== BEGIN unit/test-kernels.R =====
# test-kernels.R
# Consolidated kernel tests: registry, support table, distribution roundtrips (Tier A/B)
# Merged from: test-kernel-registry.R, test-kernel-support.R, test-kernels.R

# =============================================================================
# Kernel registry (from test-kernel-registry.R)
# =============================================================================
test_that("init_kernel_registry() returns TRUE", {
  result <- init_kernel_registry()
  expect_true(result)
  result2 <- init_kernel_registry()
  expect_true(result2)
})

test_that("get_kernel_registry() returns a complete registry", {
  registry <- get_kernel_registry()
  expect_true(is.list(registry))
  expected_kernels <- c("normal", "lognormal", "invgauss", "gamma", "laplace", "amoroso", "cauchy")
  expect_true(all(expected_kernels %in% names(registry)))
  expect_equal(length(registry), 7L)
})

test_that("Each kernel has required fields", {
  registry <- get_kernel_registry()
  for (kernel_name in names(registry)) {
    kernel <- registry[[kernel_name]]
    expect_true("key" %in% names(kernel), info = paste0("kernel '", kernel_name, "' missing 'key'"))
    expect_true("bulk_params" %in% names(kernel), info = paste0("kernel '", kernel_name, "' missing 'bulk_params'"))
    expect_true("bulk_support" %in% names(kernel), info = paste0("kernel '", kernel_name, "' missing 'bulk_support'"))
    expect_true("param_types" %in% names(kernel), info = paste0("kernel '", kernel_name, "' missing 'param_types'"))
    expect_true("allow_gpd" %in% names(kernel), info = paste0("kernel '", kernel_name, "' missing 'allow_gpd'"))
    expect_true("defaults_X" %in% names(kernel), info = paste0("kernel '", kernel_name, "' missing 'defaults_X'"))
    expect_true("sb" %in% names(kernel), info = paste0("kernel '", kernel_name, "' missing 'sb'"))
    expect_true("crp" %in% names(kernel), info = paste0("kernel '", kernel_name, "' missing 'crp'"))
    expect_true("signatures" %in% names(kernel), info = paste0("kernel '", kernel_name, "' missing 'signatures'"))
    expect_equal(kernel$key, kernel_name, info = paste0("kernel '", kernel_name, "' key mismatch"))
  }
})

test_that("Kernel bulk_params are character vectors", {
  registry <- get_kernel_registry()
  for (kernel_name in names(registry)) {
    kernel <- registry[[kernel_name]]
    expect_true(is.character(kernel$bulk_params), info = paste0("kernel '", kernel_name, "' bulk_params not character"))
    expect_true(length(kernel$bulk_params) >= 2L, info = paste0("kernel '", kernel_name, "' should have at least 2 bulk params"))
  }
})

test_that("Kernel SB signatures are properly structured", {
  registry <- get_kernel_registry()
  for (kernel_name in names(registry)) {
    kernel <- registry[[kernel_name]]
    sb <- kernel$sb
    expect_true(is.list(sb), info = paste0("kernel '", kernel_name, "' sb not a list"))
    expect_true("d" %in% names(sb), info = paste0("kernel '", kernel_name, "' sb missing 'd'"))
    expect_true("args" %in% names(sb), info = paste0("kernel '", kernel_name, "' sb missing 'args'"))
  }
})

test_that("Kernel CRP signatures are properly structured", {
  registry <- get_kernel_registry()
  for (kernel_name in names(registry)) {
    kernel <- registry[[kernel_name]]
    crp <- kernel$crp
    expect_true(is.list(crp), info = paste0("kernel '", kernel_name, "' crp not a list"))
    expect_true("d_base" %in% names(crp), info = paste0("kernel '", kernel_name, "' crp missing 'd_base'"))
  }
})

test_that("get_tail_registry() returns tail metadata", {
  tail_reg <- get_tail_registry()
  expect_true(is.list(tail_reg))
  expect_true("params" %in% names(tail_reg))
  expect_true("support" %in% names(tail_reg))
  expect_true("indexed_by_cluster_in_crp" %in% names(tail_reg))
  expect_equal(tail_reg$params, c("threshold", "tail_scale", "tail_shape"))
  expect_true(is.character(tail_reg$support))
  expect_equal(names(tail_reg$support), c("threshold", "tail_scale", "tail_shape"))
  expect_true(is.logical(tail_reg$indexed_by_cluster_in_crp))
})

test_that("Cauchy kernel does not allow GPD", {
  registry <- get_kernel_registry()
  expect_false(registry$cauchy$allow_gpd)
})

test_that("Most kernels allow GPD", {
  registry <- get_kernel_registry()
  gpd_kernels <- c("normal", "lognormal", "invgauss", "gamma", "laplace", "amoroso")
  for (k in gpd_kernels) {
    expect_true(registry[[k]]$allow_gpd, info = paste0("kernel '", k, "' should allow GPD"))
  }
})

# =============================================================================
# Kernel support table (from test-kernel-support.R)
# =============================================================================
test_that("kernel_support_table() returns a data frame", {
  result <- kernel_support_table()
  expect_true(is.data.frame(result))
})

test_that("kernel_support_table() has correct columns", {
  result <- kernel_support_table()
  expected_cols <- c("kernel", "gpd", "covariates", "sb", "crp")
  expect_equal(names(result), expected_cols)
})

test_that("kernel_support_table() includes all 7 kernels", {
  result <- kernel_support_table()
  expected_kernels <- c("normal", "lognormal", "invgauss", "gamma", "laplace", "amoroso", "cauchy")
  expect_equal(nrow(result), 7L)
  expect_true(all(expected_kernels %in% result$kernel))
})

test_that("kernel_support_table(round=TRUE) produces checkmark/cross symbols", {
  result <- kernel_support_table(round = TRUE)
  checkmark <- "\u2714"
  cross <- "\u274C"
  for (col in c("gpd", "covariates", "sb", "crp")) {
    expect_true(all(result[[col]] %in% c(checkmark, cross)), info = paste0("column '", col, "' should contain only checkmark/cross"))
  }
})

test_that("kernel_support_table(round=FALSE) produces logical values", {
  result <- kernel_support_table(round = FALSE)
  for (col in c("gpd", "covariates", "sb", "crp")) {
    expect_true(is.logical(result[[col]]), info = paste0("column '", col, "' should be logical"))
  }
})

test_that("Cauchy kernel does not support GPD in support table", {
  result <- kernel_support_table(round = FALSE)
  cauchy_row <- result[result$kernel == "cauchy", ]
  expect_false(cauchy_row$gpd)
})

test_that("All kernels support both sb and crp backends in support table", {
  result <- kernel_support_table(round = FALSE)
  expect_true(all(result$sb))
  expect_true(all(result$crp))
})

test_that("All kernels support covariates in support table", {
  result <- kernel_support_table(round = FALSE)
  expect_true(all(result$covariates))
})

test_that("Most kernels support GPD in support table (except Cauchy)", {
  result <- kernel_support_table(round = FALSE)
  gpd_kernels <- result[result$kernel != "cauchy", ]
  expect_true(all(gpd_kernels$gpd))
})

test_that("kernel_support_table() kernel column is character", {
  result <- kernel_support_table()
  expect_true(is.character(result$kernel))
})

# =============================================================================
# Kernel distribution roundtrips (from test-kernels.R)
# =============================================================================
test_that(
  "Kernels: mixture families (mix / mix+GPD / scalar+GPD) semantic + roundtrip checks",
  {
    # This test runs kernel math checks - fast, no MCMC compilation

    # ---------- helpers ----------
    .norm_w <- function(w) {
      w <- as.numeric(w)
      s <- sum(w)
      if (!is.finite(s) || s <= 0) rep(1 / length(w), length(w)) else w / s
    }

    .expect_density_ok <- function(dval, label = "") {
      expect_true(is.numeric(dval), info = label)
      expect_true(length(dval) >= 1, info = label)
      expect_true(all(is.finite(dval)), info = label)
      expect_true(all(dval >= 0), info = label)
    }

    .expect_cdf_ok <- function(pval, label = "") {
      expect_true(is.numeric(pval), info = label)
      expect_true(length(pval) >= 1, info = label)
      expect_true(all(is.finite(pval)), info = label)
      expect_true(all(pval >= 0 & pval <= 1), info = label)
    }

    .expect_rng_ok <- function(x, support = c(-Inf, Inf), label = "") {
      expect_true(is.numeric(x), info = label)
      expect_true(length(x) == 1, info = label)
      expect_true(is.finite(x), info = label)
      expect_true(x >= support[1] && x <= support[2], info = label)
    }

    # Roundtrip q(p) -> p(q) with diagnostics (realistic tolerances for numeric inversion).
    .roundtrip_qp <- function(qFun, pFun, probs, args, tol, label) {
      qv <- do.call(qFun, c(list(p = probs), args))
      expect_true(is.numeric(qv), info = paste0(label, " : qFun returned non-numeric"))
      expect_true(length(qv) == length(probs), info = paste0(label, " : qFun length mismatch"))

      okq <- is.finite(qv) & probs > 0 & probs < 1
      if (!any(okq)) {
        succeed(paste0(label, " : roundtrip skipped (no finite q)"))
        return(invisible(TRUE))
      }

      # IMPORTANT: for nimbleFunctions, pass integer flags (1/0), not TRUE/FALSE.
      pb <- vapply(
        qv[okq],
        function(qq) do.call(pFun, c(list(q = qq), args, list(lower.tail = 1L, log.p = 0L))),
        numeric(1)
      )

      if (any(!is.finite(pb))) {
        bad <- which(!is.finite(pb))[1]
        msg <- paste0(
          label, " : pFun returned non-finite at index ", bad,
          " (p=", probs[okq][bad], ", q=", qv[okq][bad], ")."
        )
        fail(msg)
      }

      .expect_cdf_ok(pb, label = paste0(label, " : pFun range"))

      err <- max(abs(pb - probs[okq]))
      expect_true(
        err < tol,
        info = paste0(label, " : max|p_back - p| = ", signif(err, 6), " (tol=", tol, ")")
      )
      invisible(TRUE)
    }

    .check_mix_family <- function(
    name,
    support = c(-Inf, Inf),
    dMix, pMix, rMix, qMix,
    args_mix,
    grid = NULL,
    dMixGpd = NULL, pMixGpd = NULL, rMixGpd = NULL, qMixGpd = NULL,
    args_mixgpd = NULL,
    dScalarGpd = NULL, pScalarGpd = NULL, rScalarGpd = NULL, qScalarGpd = NULL,
    args_scalargpd = NULL,
    tol_mix = 1e-4,
    tol_splice = 5e-4
    ) {
      # normalize weights for stability
      if ("w" %in% names(args_mix)) args_mix$w <- .norm_w(args_mix$w)
      if (!is.null(args_mixgpd) && "w" %in% names(args_mixgpd)) args_mixgpd$w <- .norm_w(args_mixgpd$w)

      if (is.null(grid)) {
        grid <- if (is.finite(support[1])) support[1] + c(0.01, 0.2, 1, 2) else c(-2, -0.5, 0, 0.5, 2)
        grid <- grid[grid >= support[1]]
        if (length(grid) == 0) grid <- support[1] + 0.5
      }

      # --- MIX checks ---
      dvals <- vapply(grid, function(xx) do.call(dMix, c(list(x = xx), args_mix, list(log = 0L))), numeric(1))
      pvals <- vapply(grid, function(xx) do.call(pMix, c(list(q = xx), args_mix, list(lower.tail = 1L, log.p = 0L))), numeric(1))

      .expect_density_ok(dvals, label = paste0(name, " : dMix"))
      .expect_cdf_ok(pvals, label = paste0(name, " : pMix"))

      r1 <- do.call(rMix, c(list(n = 1L), args_mix))
      .expect_rng_ok(r1, support = support, label = paste0(name, " : rMix"))

      probs <- c(0.05, 0.2, 0.5, 0.8, 0.95)
      .roundtrip_qp(qMix, pMix, probs, args_mix, tol = tol_mix, label = paste0(name, " : mix roundtrip"))

      succeed(paste0("mix checks ok: ", name))

      # --- MIX+GPD checks ---
      if (!is.null(dMixGpd) && !is.null(pMixGpd) && !is.null(rMixGpd) && !is.null(qMixGpd)) {
        expect_true(!is.null(args_mixgpd), info = paste0(name, " : args_mixgpd missing"))

        u <- args_mixgpd$threshold

        # continuity at threshold: F_mix(u) == F_mixgpd(u)
        Fu0 <- do.call(pMix,    c(list(q = u), args_mix,    list(lower.tail = 1L, log.p = 0L)))
        Fu1 <- do.call(pMixGpd, c(list(q = u), args_mixgpd, list(lower.tail = 1L, log.p = 0L)))

        .expect_cdf_ok(Fu0, label = paste0(name, " : Fu0"))
        .expect_cdf_ok(Fu1, label = paste0(name, " : Fu1"))

        expect_equal(
          as.numeric(Fu1), as.numeric(Fu0),
          tolerance = 1e-6,
          info = paste0(name, " : continuity at threshold")
        )

        grid2 <- sort(unique(c(u - 0.1, u, u + 0.1, u + 1)))
        d2 <- vapply(grid2, function(xx) do.call(dMixGpd, c(list(x = xx), args_mixgpd, list(log = 0L))), numeric(1))
        p2 <- vapply(grid2, function(xx) do.call(pMixGpd, c(list(q = xx), args_mixgpd, list(lower.tail = 1L, log.p = 0L))), numeric(1))

        if (any(!is.finite(p2))) {
          bad <- which(!is.finite(p2))[1]
          fail(paste0(
            name, " : pMixGpd produced non-finite at q=", grid2[bad],
            " (threshold=", u, ")."
          ))
        }

        .expect_density_ok(d2, label = paste0(name, " : dMixGpd"))
        .expect_cdf_ok(p2, label = paste0(name, " : pMixGpd"))

        r2 <- do.call(rMixGpd, c(list(n = 1L), args_mixgpd))
        .expect_rng_ok(r2, support = support, label = paste0(name, " : rMixGpd"))

        probs2 <- c(0.05, 0.2, 0.5, 0.9, 0.98)
        .roundtrip_qp(qMixGpd, pMixGpd, probs2, args_mixgpd, tol = tol_splice, label = paste0(name, " : mixGpd roundtrip"))

        succeed(paste0("mix+GPD checks ok: ", name))
      }

      # --- Scalar+GPD checks (if present) ---
      if (!is.null(dScalarGpd) && !is.null(pScalarGpd) && !is.null(rScalarGpd) && !is.null(qScalarGpd)) {
        expect_true(!is.null(args_scalargpd), info = paste0(name, " : args_scalargpd missing"))

        u <- args_scalargpd$threshold
        grid3 <- sort(unique(c(u - 0.1, u, u + 0.1, u + 1)))

        d3 <- vapply(grid3, function(xx) do.call(dScalarGpd, c(list(x = xx), args_scalargpd, list(log = 0L))), numeric(1))
        p3 <- vapply(grid3, function(xx) do.call(pScalarGpd, c(list(q = xx), args_scalargpd, list(lower.tail = 1L, log.p = 0L))), numeric(1))

        if (any(!is.finite(p3))) {
          bad <- which(!is.finite(p3))[1]
          fail(paste0(
            name, " : pScalarGpd produced non-finite at q=", grid3[bad],
            " (threshold=", u, ")."
          ))
        }

        .expect_density_ok(d3, label = paste0(name, " : dScalarGpd"))
        .expect_cdf_ok(p3, label = paste0(name, " : pScalarGpd"))

        r3 <- do.call(rScalarGpd, c(list(n = 1L), args_scalargpd))
        .expect_rng_ok(r3, support = support, label = paste0(name, " : rScalarGpd"))

        probs3 <- c(0.05, 0.2, 0.5, 0.9, 0.98)
        .roundtrip_qp(qScalarGpd, pScalarGpd, probs3, args_scalargpd, tol = tol_splice, label = paste0(name, " : scalarGpd roundtrip"))

        succeed(paste0("scalar+GPD checks ok: ", name))
      }

      invisible(TRUE)
    }

    # ---------- shared ----------
    # 3-component mixtures everywhere (per your rule)
    w3 <- c(0.50, 0.30, 0.20)

    gpd_tail <- list(
      threshold  = 2.0,
      tail_scale = 1.0,
      tail_shape = 0.2
    )

    # Gamma
    .check_mix_family(
      name = "Gamma",
      support = c(0, Inf),
      dMix = dGammaMix, pMix = pGammaMix, rMix = rGammaMix, qMix = qGammaMix,
      dMixGpd = dGammaMixGpd, pMixGpd = pGammaMixGpd, rMixGpd = rGammaMixGpd, qMixGpd = qGammaMixGpd,
      dScalarGpd = dGammaGpd, pScalarGpd = pGammaGpd, rScalarGpd = rGammaGpd, qScalarGpd = qGammaGpd,
      args_mix = list(w = w3, shape = c(2, 4, 6), scale = c(1, 1.5, 2.0)),
      args_mixgpd = c(list(w = w3, shape = c(2, 4, 6), scale = c(1, 1.5, 2.0)), gpd_tail),
      args_scalargpd = c(list(shape = 3, scale = 1), gpd_tail),
      grid = c(0.1, 0.5, 1, 2, 4, 6),
      tol_mix = 5e-4, tol_splice = 1e-3
    )

    # Normal
    .check_mix_family(
      name = "Normal",
      support = c(-Inf, Inf),
      dMix = dNormMix, pMix = pNormMix, rMix = rNormMix, qMix = qNormMix,
      dMixGpd = dNormMixGpd, pMixGpd = pNormMixGpd, rMixGpd = rNormMixGpd, qMixGpd = qNormMixGpd,
      dScalarGpd = dNormGpd, pScalarGpd = pNormGpd, rScalarGpd = rNormGpd, qScalarGpd = qNormGpd,
      args_mix = list(w = w3, mean = c(-1, 1, 3), sd = c(1, 0.8, 0.6)),
      args_mixgpd = c(list(w = w3, mean = c(-1, 1, 3), sd = c(1, 0.8, 0.6)), gpd_tail),
      args_scalargpd = c(list(mean = 0, sd = 1), gpd_tail),
      grid = c(-3, -1, 0, 1, 3, 5),
      tol_mix = 5e-4, tol_splice = 1e-3
    )

    # Lognormal
    .check_mix_family(
      name = "Lognormal",
      support = c(0, Inf),
      dMix = dLognormalMix, pMix = pLognormalMix, rMix = rLognormalMix, qMix = qLognormalMix,
      dMixGpd = dLognormalMixGpd, pMixGpd = pLognormalMixGpd, rMixGpd = rLognormalMixGpd, qMixGpd = qLognormalMixGpd,
      dScalarGpd = dLognormalGpd, pScalarGpd = pLognormalGpd, rScalarGpd = rLognormalGpd, qScalarGpd = qLognormalGpd,
      args_mix = list(w = w3, meanlog = c(-0.2, 0.6, 1.1), sdlog = c(0.7, 0.5, 0.4)),
      args_mixgpd = c(list(w = w3, meanlog = c(-0.2, 0.6, 1.1), sdlog = c(0.7, 0.5, 0.4)), gpd_tail),
      args_scalargpd = c(list(meanlog = 0, sdlog = 0.6), gpd_tail),
      grid = c(0.1, 0.5, 1, 2, 5, 10),
      tol_mix = 5e-4, tol_splice = 1e-3
    )

    # Laplace
    .check_mix_family(
      name = "Laplace",
      support = c(-Inf, Inf),
      dMix = dLaplaceMix, pMix = pLaplaceMix, rMix = rLaplaceMix, qMix = qLaplaceMix,
      dMixGpd = dLaplaceMixGpd, pMixGpd = pLaplaceMixGpd, rMixGpd = rLaplaceMixGpd, qMixGpd = qLaplaceMixGpd,
      dScalarGpd = dLaplaceGpd, pScalarGpd = pLaplaceGpd, rScalarGpd = rLaplaceGpd, qScalarGpd = qLaplaceGpd,
      args_mix = list(w = w3, location = c(-1, 1, 3), scale = c(1.2, 0.8, 0.6)),
      args_mixgpd = c(list(w = w3, location = c(-1, 1, 3), scale = c(1.2, 0.8, 0.6)), gpd_tail),
      args_scalargpd = c(list(location = 0, scale = 1), gpd_tail),
      grid = c(-3, -1, 0, 1, 3, 5),
      tol_mix = 5e-4, tol_splice = 1e-3
    )

    # Inverse Gaussian
    .check_mix_family(
      name = "InvGauss",
      support = c(0, Inf),
      dMix = dInvGaussMix, pMix = pInvGaussMix, rMix = rInvGaussMix, qMix = qInvGaussMix,
      dMixGpd = dInvGaussMixGpd, pMixGpd = pInvGaussMixGpd, rMixGpd = rInvGaussMixGpd, qMixGpd = qInvGaussMixGpd,
      dScalarGpd = dInvGaussGpd, pScalarGpd = pInvGaussGpd, rScalarGpd = rInvGaussGpd, qScalarGpd = qInvGaussGpd,
      args_mix = list(w = w3, mean = c(0.8, 1.6, 2.4), shape = c(1.5, 2.5, 3.5)),
      args_mixgpd = c(list(w = w3, mean = c(0.8, 1.6, 2.4), shape = c(1.5, 2.5, 3.5)), gpd_tail),
      args_scalargpd = c(list(mean = 1.5, shape = 2.0), gpd_tail),
      grid = c(0.1, 0.5, 1, 2, 4, 8),
      tol_mix = 1e-3, tol_splice = 2e-3
    )

    # Amoroso
    .check_mix_family(
      name = "Amoroso",
      support = c(0, Inf),
      dMix = dAmorosoMix, pMix = pAmorosoMix, rMix = rAmorosoMix, qMix = qAmorosoMix,
      dMixGpd = dAmorosoMixGpd, pMixGpd = pAmorosoMixGpd, rMixGpd = rAmorosoMixGpd, qMixGpd = qAmorosoMixGpd,
      dScalarGpd = dAmorosoGpd, pScalarGpd = pAmorosoGpd, rScalarGpd = rAmorosoGpd, qScalarGpd = qAmorosoGpd,
      args_mix = list(
        w = w3,
        loc = c(0, 0.3, 0.6),
        scale = c(1, 1, 1),
        shape1 = c(2, 3, 5),
        shape2 = c(1.0, 1.3, 1.6)
      ),
      args_mixgpd = c(list(
        w = w3,
        loc = c(0, 0.3, 0.6),
        scale = c(1, 1, 1),
        shape1 = c(2, 3, 5),
        shape2 = c(1.0, 1.3, 1.6)
      ), gpd_tail),
      args_scalargpd = c(list(loc = 0, scale = 1, shape1 = 2, shape2 = 1.3), gpd_tail),
      grid = c(0.01, 0.2, 0.8, 1.5, 3, 6),
      tol_mix = 1e-3, tol_splice = 2e-3
    )

    # Cauchy (mix only)
    .check_mix_family(
      name = "Cauchy",
      support = c(-Inf, Inf),
      dMix = dCauchyMix, pMix = pCauchyMix, rMix = rCauchyMix, qMix = qCauchyMix,
      args_mix = list(w = w3, location = c(-1, 1, 3), scale = c(1.2, 0.8, 0.6)),
      grid = c(-5, -2, -1, 0, 1, 3, 6),
      tol_mix = 1e-3
    )
  }
)
test_that("All nimble::nimbleFunction-defined objects in R/ compile", {
  # Keep this out of Tier A (cran) because it's expensive; allow it for CI + coverage.
  skip_if_not_test_level("ci")

  r_dir <- test_path("..", "..", "R")
  expect_true(dir.exists(r_dir), info = paste("Missing R dir:", r_dir))

  r_files <- list.files(r_dir, pattern = "\\.[Rr]$", full.names = TRUE)
  expect_true(length(r_files) > 0, info = "No R files found under R/")

  # Find:  NAME <- nimble::nimbleFunction(   OR   NAME <- nimbleFunction(
  find_defs <- function(files) {
    rx <- "^\\s*([.A-Za-z][.A-Za-z0-9_]*)\\s*<-\\s*(nimble::)?nimbleFunction\\s*\\("
    out <- list()
    for (ff in files) {
      lines <- readLines(ff, warn = FALSE)
      hit <- grepl(rx, lines)
      if (any(hit)) {
        idx <- which(hit)
        nm  <- sub(rx, "\\1", lines[idx], perl = TRUE)
        out[[ff]] <- data.frame(
          name = nm,
          file = basename(ff),
          line = idx,
          stringsAsFactors = FALSE
        )
      }
    }
    if (!length(out)) {
      return(data.frame(name = character(), file = character(), line = integer()))
    }
    do.call(rbind, out)
  }

  defs <- find_defs(r_files)
  expect_true(nrow(defs) > 0, info = "No nimbleFunction definitions found in R/")

  # Use the package namespace so dependencies among nimbleFunctions are resolved.
  ns <- tryCatch(asNamespace("CausalMixGPD"), error = function(e) NULL)
  if (is.null(ns)) {
    skip("CausalMixGPD namespace not available for compile checks.")
  }

  # Pretty status symbols (colored if crayon is available)
  tick  <- if (requireNamespace("crayon", quietly = TRUE))
    crayon::green("\u2714") else "\u2714"
  cross <- if (requireNamespace("crayon", quietly = TRUE))
    crayon::red("\u2718") else "\u2718"

  failures <- character(0)
  results <- data.frame(
    status = character(),
    name   = character(),
    where  = character(),
    detail = character(),
    stringsAsFactors = FALSE
  )

  for (i in seq_len(nrow(defs))) {
    nm <- defs$name[i]
    where <- paste0(defs$file[i], ":", defs$line[i])

    if (!exists(nm, envir = ns, inherits = FALSE)) {
      msg <- paste0("object not found in package namespace")
      results <- rbind(results, data.frame(
        status = cross, name = nm, where = where, detail = msg,
        stringsAsFactors = FALSE
      ))
      failures <- c(failures, paste0("- ", nm, " @ ", where, " :: ", msg))
      next
    }

    obj <- get(nm, envir = ns, inherits = FALSE)

    # If a nimbleFunction was wrapped for vectorization, compile its *_nf alias.
    if (!inherits(obj, "nimbleFunction")) {
      nf_name <- paste0(nm, "_nf")
      if (exists(nf_name, envir = ns, inherits = FALSE)) {
        obj <- get(nf_name, envir = ns, inherits = FALSE)
      }
    }

    err <- tryCatch({
      suppressWarnings(nimble::compileNimble(obj))
      NULL
    }, error = function(e) e)

    if (is.null(err)) {
      # Per-object PASS entry in testthat output
      expect_true(TRUE, info = paste0("compileNimble succeeded for ", nm, " @ ", where))

      results <- rbind(results, data.frame(
        status = tick, name = nm, where = where, detail = "",
        stringsAsFactors = FALSE
      ))
    } else {
      e1 <- strsplit(conditionMessage(err), "\n", fixed = TRUE)[[1]][1]
      cls <- paste(class(obj), collapse = "/")
      msg <- paste0("class=", cls, " :: ", e1)

      results <- rbind(results, data.frame(
        status = cross, name = nm, where = where, detail = msg,
        stringsAsFactors = FALSE
      ))
      failures <- c(failures, paste0("- ", nm, " @ ", where, " :: ", msg))
    }
  }

  # Print a compact summary table
  cat("\nCompilation summary (nimbleFunction -> compileNimble):\n")
  print(results[, c("status", "name", "where")], row.names = FALSE)

  # If there are failures, print details and fail the test
  if (length(failures) > 0) {
    cat("\nFailure details:\n")
    cat(paste0(failures, collapse = "\n"), "\n")
    fail(paste(
      "The following nimbleFunction-defined objects failed to compile:",
      paste(failures, collapse = "\n"),
      sep = "\n"
    ))
  }
})

# ===== END unit/test-kernels.R =====

# ===== BEGIN unit/test-s3-methods.R =====
# tests/testthat/test-s3-methods.R
# Unit tests for S3 print/summary/plot methods (04-S3-Methods.R)
# Uses minimal mock objects to avoid heavy MCMC runs

pkg <- "CausalMixGPD"
print_mixgpd_params <- getFromNamespace("print.mixgpd_params", pkg)
print_mixgpd_params_pair <- getFromNamespace("print.mixgpd_params_pair", pkg)
print_causalmixgpd_qte <- getFromNamespace("print.causalmixgpd_qte", pkg)
print_causalmixgpd_ate <- getFromNamespace("print.causalmixgpd_ate", pkg)
summary_causalmixgpd_qte <- getFromNamespace("summary.causalmixgpd_qte", pkg)
summary_causalmixgpd_ate <- getFromNamespace("summary.causalmixgpd_ate", pkg)
print_summary_causalmixgpd_qte <- getFromNamespace("print.summary.causalmixgpd_qte", pkg)
print_summary_causalmixgpd_ate <- getFromNamespace("print.summary.causalmixgpd_ate", pkg)
print_summary_causalmixgpd_ps_fit <- getFromNamespace("print.summary.causalmixgpd_ps_fit", pkg)
print_summary_causalmixgpd_causal_fit <- getFromNamespace("print.summary.causalmixgpd_causal_fit", pkg)
plot_mixgpd_predict <- getFromNamespace("plot.mixgpd_predict", pkg)
print_causalmixgpd_causal_predict_plots <- getFromNamespace("print.causalmixgpd_causal_predict_plots", pkg)
print_mixgpd_predict_plots <- getFromNamespace("print.mixgpd_predict_plots", pkg)
print_mixgpd_fit_plots <- getFromNamespace("print.mixgpd_fit_plots", pkg)
print_causalmixgpd_causal_fit_plots <- getFromNamespace("print.causalmixgpd_causal_fit_plots", pkg)
plot_mixgpd_fitted <- getFromNamespace("plot.mixgpd_fitted", pkg)
print_mixgpd_fitted_plots <- getFromNamespace("print.mixgpd_fitted_plots", pkg)
plot_causalmixgpd_qte <- getFromNamespace("plot.causalmixgpd_qte", pkg)
plot_causalmixgpd_ate <- getFromNamespace("plot.causalmixgpd_ate", pkg)

# ======================================================================
# Mock object constructors
# ======================================================================

make_mock_mixgpd_params <- function(empty = FALSE) {
  if (empty) {
    out <- list()
  } else {
    out <- list(
      alpha = 1.5,
      w = c(0.6, 0.3, 0.1),
      mu = c(1, 2, 3),
      sigma = c(0.5, 1, 1.5),
      beta_mu = matrix(c(0.1, 0.2, 0.3, 0.4, 0.5, 0.6), nrow = 3, ncol = 2,
                       dimnames = list(paste0("comp", 1:3), c("x1", "x2")))
    )
  }
  class(out) <- "mixgpd_params"
  out
}

make_mock_mixgpd_params_pair <- function() {
  out <- list(
    treated = make_mock_mixgpd_params(),
    control = make_mock_mixgpd_params()
  )
  class(out) <- "mixgpd_params_pair"
  out
}

make_mock_qte <- function() {
  out <- list(
    probs = c(0.25, 0.5, 0.75),
    grid = c(0.25, 0.5, 0.75),
    n_pred = 5,
    level = 0.95,
    interval = "credible",
    meta = list(backend = list(trt = "sb", con = "sb"),
                kernel = list(trt = "normal", con = "normal"),
                GPD = list(trt = FALSE, con = FALSE)),
    ps = runif(5),
    x = matrix(rnorm(10), nrow = 5, ncol = 2),
    qte = list(
      fit = data.frame(
        id = rep(1:5, each = 3),
        index = rep(c(0.25, 0.5, 0.75), times = 5),
        estimate = rnorm(15),
        lower = rnorm(15) - 0.5,
        upper = rnorm(15) + 0.5
      )
    ),
    fit = matrix(rnorm(15), nrow = 5, ncol = 3),
    lower = matrix(rnorm(15) - 0.5, nrow = 5, ncol = 3),
    upper = matrix(rnorm(15) + 0.5, nrow = 5, ncol = 3),
    trt = list(fit = data.frame(estimate = rnorm(15), id = rep(1:5, 3), index = rep(c(0.25, 0.5, 0.75), each = 5))),
    con = list(fit = data.frame(estimate = rnorm(15), id = rep(1:5, 3), index = rep(c(0.25, 0.5, 0.75), each = 5)))
  )
  class(out) <- "causalmixgpd_qte"
  out
}

make_mock_ate <- function() {
  out <- list(
    n_pred = 5,
    level = 0.95,
    interval = "credible",
    nsim_mean = 200,
    meta = list(backend = list(trt = "sb", con = "sb"),
                kernel = list(trt = "normal", con = "normal"),
                GPD = list(trt = FALSE, con = FALSE)),
    ps = runif(5),
    x = matrix(rnorm(10), nrow = 5, ncol = 2),
    ate = list(
      fit = data.frame(
        id = 1:5,
        estimate = rnorm(5),
        lower = rnorm(5) - 0.5,
        upper = rnorm(5) + 0.5
      )
    ),
    fit = rnorm(5),
    lower = rnorm(5) - 0.5,
    upper = rnorm(5) + 0.5,
    trt = list(fit = data.frame(estimate = rnorm(5), id = 1:5)),
    con = list(fit = data.frame(estimate = rnorm(5), id = 1:5))
  )
  class(out) <- "causalmixgpd_ate"
  out
}

make_mock_mixgpd_predict <- function(type = "quantile") {
  if (type == "quantile") {
    out <- list(
      fit = data.frame(
        estimate = c(1, 2, 3),
        index = c(0.25, 0.5, 0.75),
        lower = c(0.5, 1.5, 2.5),
        upper = c(1.5, 2.5, 3.5)
      ),
      type = "quantile",
      grid = c(0.25, 0.5, 0.75)
    )
  } else if (type == "sample") {
    out <- list(
      fit = rnorm(100),
      type = "sample",
      grid = NULL
    )
  } else if (type == "mean") {
    out <- list(
      fit = data.frame(estimate = 5.2, lower = 4.8, upper = 5.6),
      type = "mean",
      grid = NULL
    )
  } else if (type == "density") {
    y_grid <- seq(0, 5, length.out = 20)
    out <- list(
      fit = data.frame(
        id = rep(1, 20),
        y = y_grid,
        density = dnorm(y_grid, mean = 2.5, sd = 1),
        lower = dnorm(y_grid, mean = 2.5, sd = 1) - 0.05,
        upper = dnorm(y_grid, mean = 2.5, sd = 1) + 0.05
      ),
      type = "density",
      grid = y_grid
    )
  } else if (type == "survival") {
    y_grid <- seq(0, 5, length.out = 20)
    out <- list(
      fit = data.frame(
        id = rep(1, 20),
        y = y_grid,
        survival = 1 - pnorm(y_grid, mean = 2.5, sd = 1),
        lower = pmax(0, 1 - pnorm(y_grid, mean = 2.5, sd = 1) - 0.05),
        upper = pmin(1, 1 - pnorm(y_grid, mean = 2.5, sd = 1) + 0.05)
      ),
      type = "survival",
      grid = y_grid
    )
  } else if (type == "location") {
    out <- list(
      fit = data.frame(
        mean = 5.0, mean_lower = 4.5, mean_upper = 5.5,
        median = 4.9, median_lower = 4.4, median_upper = 5.4
      ),
      type = "location",
      grid = NULL
    )
  } else {
    stop("Unknown mock predict type")
  }
  class(out) <- "mixgpd_predict"
  out
}

make_mock_causal_predict_plots <- function() {
  out <- list(
    trt_control = ggplot2::ggplot() + ggplot2::ggtitle("trt_control"),
    treatment_effect = ggplot2::ggplot() + ggplot2::ggtitle("treatment_effect")
  )
  class(out) <- c("causalmixgpd_causal_predict_plots", "list")
  out
}

make_mock_mixgpd_predict_plots <- function() {
  p <- ggplot2::ggplot() + ggplot2::ggtitle("predict_plot")
  class(p) <- c("mixgpd_predict_plots", class(p))
  p
}

make_mock_mixgpd_fit_plots <- function() {
  out <- list(
    traceplot = ggplot2::ggplot() + ggplot2::ggtitle("traceplot"),
    density = ggplot2::ggplot() + ggplot2::ggtitle("density")
  )
  class(out) <- c("mixgpd_fit_plots", "list")
  out
}

make_mock_mixgpd_fitted <- function() {
  n <- 20
  fit_vals <- rnorm(n, mean = 5)
  y_obs <- fit_vals + rnorm(n, sd = 0.5)
  out <- data.frame(
    fit = fit_vals,
    lower = fit_vals - 0.5,
    upper = fit_vals + 0.5,
    residuals = y_obs - fit_vals
  )
  class(out) <- c("mixgpd_fitted", "data.frame")

  # Attach a minimal mock object

mock_obj <- list(
    data = list(y = y_obs),
    spec = list(meta = list(backend = "sb", kernel = "normal", GPD = FALSE))
  )
  attr(out, "object") <- mock_obj
  attr(out, "level") <- 0.95
  attr(out, "interval") <- "credible"
  out
}

make_mock_causal_fit_plots <- function() {
  treated_plots <- list(
    traceplot = ggplot2::ggplot() + ggplot2::ggtitle("treated_trace"),
    density = ggplot2::ggplot() + ggplot2::ggtitle("treated_density")
  )
  class(treated_plots) <- c("mixgpd_fit_plots", "list")

  control_plots <- list(
    traceplot = ggplot2::ggplot() + ggplot2::ggtitle("control_trace"),
    density = ggplot2::ggplot() + ggplot2::ggtitle("control_density")
  )
  class(control_plots) <- c("mixgpd_fit_plots", "list")

  out <- list(treated = treated_plots, control = control_plots)
  class(out) <- c("causalmixgpd_causal_fit_plots", "list")
  out
}

make_mock_summary_qte <- function() {
  qte_obj <- make_mock_qte()
  out <- list(
    overall = list(
      n_pred = 5,
      n_quantiles = 3,
      quantiles = c(0.25, 0.5, 0.75),
      level = 0.95,
      interval = "credible",
      has_covariates = TRUE,
      ps_used = TRUE
    ),
    quantile_summary = data.frame(
      quantile = c(0.25, 0.5, 0.75),
      mean_qte = c(0.1, 0.2, 0.3),
      median_qte = c(0.1, 0.2, 0.3),
      min_qte = c(-0.1, 0, 0.1),
      max_qte = c(0.3, 0.4, 0.5),
      sd_qte = c(0.1, 0.1, 0.1)
    ),
    ci_summary = list(
      mean_width = 0.5,
      median_width = 0.5,
      min_width = 0.3,
      max_width = 0.7
    ),
    meta = list(
      backend = list(trt = "sb", con = "sb"),
      kernel = list(trt = "normal", con = "normal"),
      GPD = list(trt = FALSE, con = FALSE)
    ),
    object = qte_obj
  )
  class(out) <- "summary.causalmixgpd_qte"
  out
}

make_mock_summary_ate <- function() {
  ate_obj <- make_mock_ate()
  out <- list(
    overall = list(
      n_pred = 5,
      level = 0.95,
      interval = "credible",
      nsim_mean = 200,
      has_covariates = TRUE,
      ps_used = TRUE
    ),
    ate_stats = list(
      mean_ate = 0.5,
      median_ate = 0.45,
      min_ate = 0.1,
      max_ate = 0.9,
      sd_ate = 0.2
    ),
    ci_summary = list(
      mean_width = 0.5,
      median_width = 0.5,
      min_width = 0.3,
      max_width = 0.7
    ),
    meta = list(
      backend = list(trt = "sb", con = "sb"),
      kernel = list(trt = "normal", con = "normal"),
      GPD = list(trt = FALSE, con = FALSE)
    ),
    object = ate_obj
  )
  class(out) <- "summary.causalmixgpd_ate"
  out
}

make_mock_mixgpd_fitted_plots <- function() {
  out <- list(
    observed_fitted_plot = ggplot2::ggplot() + ggplot2::ggtitle("obs_fit"),
    residual_plot = ggplot2::ggplot() + ggplot2::ggtitle("residuals")
  )
  class(out) <- c("mixgpd_fitted_plots", "list")
  out
}

# ======================================================================
# print.mixgpd_params tests
# ======================================================================

test_that("print.mixgpd_params works with non-empty params", {
  params <- make_mock_mixgpd_params()
  expect_output(print_mixgpd_params(params), "Posterior mean parameters")
  expect_output(print_mixgpd_params(params), "alpha")
  expect_output(print_mixgpd_params(params), "beta_mu")
})

test_that("print.mixgpd_params works with empty params", {
  params <- make_mock_mixgpd_params(empty = TRUE)
  expect_output(print_mixgpd_params(params), "empty")
})

test_that("print.mixgpd_params respects digits argument", {
  params <- make_mock_mixgpd_params()
  expect_output(print_mixgpd_params(params, digits = 2), regexp = "Posterior mean parameters")
})

# ======================================================================
# print.mixgpd_params_pair tests
# ======================================================================

test_that("print.mixgpd_params_pair works", {
  params_pair <- make_mock_mixgpd_params_pair()
  expect_output(print_mixgpd_params_pair(params_pair), "Posterior mean parameters \\(causal\\)")
  expect_output(print_mixgpd_params_pair(params_pair), "treated")
  expect_output(print_mixgpd_params_pair(params_pair), "control")
})

# ======================================================================
# print.causalmixgpd_qte tests
# ======================================================================

test_that("print.causalmixgpd_qte works", {
  qte <- make_mock_qte()
  expect_output(print_causalmixgpd_qte(qte), "QTE \\(Quantile Treatment Effect\\)")
  expect_output(print_causalmixgpd_qte(qte), "Prediction points")
  expect_output(print_causalmixgpd_qte(qte), "Quantile grid")
})

test_that("print.causalmixgpd_qte respects max_rows", {
  qte <- make_mock_qte()
  expect_output(print_causalmixgpd_qte(qte, max_rows = 2), "more rows")
})

test_that("print.causalmixgpd_qte uses estimand-specific labels", {
  qte <- make_mock_qte()
  qte$type <- "cqte"
  expect_output(print_causalmixgpd_qte(qte), "CQTE")
  qte$type <- "qtt"
  expect_output(print_causalmixgpd_qte(qte), "QTT")
})

# ======================================================================
# print.causalmixgpd_ate tests
# ======================================================================

test_that("print.causalmixgpd_ate works", {
  ate <- make_mock_ate()
  expect_output(print_causalmixgpd_ate(ate), "ATE \\(Average Treatment Effect\\)")
  expect_output(print_causalmixgpd_ate(ate), "Prediction points")
})

test_that("print.causalmixgpd_ate respects max_rows", {
  ate <- make_mock_ate()
  expect_output(print_causalmixgpd_ate(ate, max_rows = 2), "more rows")
})

test_that("print.causalmixgpd_ate uses estimand-specific labels", {
  ate <- make_mock_ate()
  ate$type <- "cate"
  expect_output(print_causalmixgpd_ate(ate), "CATE")
  ate$type <- "att"
  expect_output(print_causalmixgpd_ate(ate), "ATT")
})

# ======================================================================
# summary.causalmixgpd_qte tests
# ======================================================================

test_that("summary.causalmixgpd_qte returns proper structure", {
  qte <- make_mock_qte()
  summ <- summary_causalmixgpd_qte(qte)
  expect_s3_class(summ, "summary.causalmixgpd_qte")
  expect_true("overall" %in% names(summ))
  expect_true("quantile_summary" %in% names(summ))
})

test_that("summary.causalmixgpd_qte prefers profile over id for conditional tables", {
  qte <- make_mock_qte()
  qte$type <- "cqte"
  qte$profile <- paste("Profile", 1:5)
  qte$qte$fit$profile <- rep(qte$profile, each = 3)

  summ <- summary_causalmixgpd_qte(qte)
  expect_true("profile" %in% names(summ$effect_table))
  expect_false("id" %in% names(summ$effect_table))
})

# ======================================================================
# summary.causalmixgpd_ate tests
# ======================================================================

test_that("summary.causalmixgpd_ate returns proper structure", {
  ate <- make_mock_ate()
  summ <- summary_causalmixgpd_ate(ate)
  expect_s3_class(summ, "summary.causalmixgpd_ate")
  expect_true("overall" %in% names(summ))
  expect_true("ate_stats" %in% names(summ))
})

test_that("summary.causalmixgpd_ate prefers profile over id for conditional tables", {
  ate <- make_mock_ate()
  ate$type <- "cate"
  ate$profile <- paste("Profile", 1:5)
  ate$ate$fit$profile <- ate$profile

  summ <- summary_causalmixgpd_ate(ate)
  expect_true("profile" %in% names(summ$effect_table))
  expect_false("id" %in% names(summ$effect_table))
})

# ======================================================================
# print.summary.causalmixgpd_qte tests
# ======================================================================

test_that("print.summary.causalmixgpd_qte works", {
  summ <- make_mock_summary_qte()
  expect_output(print_summary_causalmixgpd_qte(summ), "QTE Summary")
  expect_output(print_summary_causalmixgpd_qte(summ), "Prediction points")
  expect_output(print_summary_causalmixgpd_qte(summ), "Quantile grid")
  expect_output(print_summary_causalmixgpd_qte(summ), "Model specification")
})

test_that("print.summary.causalmixgpd_qte respects digits", {
  summ <- make_mock_summary_qte()
  expect_output(print_summary_causalmixgpd_qte(summ, digits = 2), "QTE Summary")
})

test_that("print.summary.causalmixgpd_qte uses estimand-specific labels", {
  summ <- make_mock_summary_qte()
  summ$object$type <- "cqte"
  expect_output(print_summary_causalmixgpd_qte(summ), "CQTE Summary")
})

# ======================================================================
# print.summary.causalmixgpd_ate tests
# ======================================================================

test_that("print.summary.causalmixgpd_ate works", {
  summ <- make_mock_summary_ate()
  expect_output(print_summary_causalmixgpd_ate(summ), "ATE Summary")
  expect_output(print_summary_causalmixgpd_ate(summ), "Prediction points")
  expect_output(print_summary_causalmixgpd_ate(summ), "Model specification")
})

test_that("print.summary.causalmixgpd_ate respects digits", {
  summ <- make_mock_summary_ate()
  expect_output(print_summary_causalmixgpd_ate(summ, digits = 2), "ATE Summary")
})

test_that("print.summary.causalmixgpd_ate uses estimand-specific labels", {
  summ <- make_mock_summary_ate()
  summ$object$type <- "att"
  expect_output(print_summary_causalmixgpd_ate(summ), "ATT Summary")
})

# ======================================================================
# plot.mixgpd_predict tests
# ======================================================================

test_that("plot.mixgpd_predict works for quantile", {
  skip_if_not_installed("ggplot2")
  pred <- make_mock_mixgpd_predict("quantile")
  p <- plot_mixgpd_predict(pred)
  expect_true(inherits(p, "gg") || inherits(p, "ggplot"))
})

test_that("plot.mixgpd_predict works for sample", {
  skip_if_not_installed("ggplot2")
  pred <- make_mock_mixgpd_predict("sample")
  p <- plot_mixgpd_predict(pred)
  expect_true(inherits(p, "gg") || inherits(p, "ggplot"))
})

test_that("plot.mixgpd_predict works for mean", {
  skip_if_not_installed("ggplot2")
  pred <- make_mock_mixgpd_predict("mean")
  p <- plot_mixgpd_predict(pred)
  expect_true(inherits(p, "gg") || inherits(p, "ggplot"))
})

test_that("plot.mixgpd_predict works for density", {
  skip_if_not_installed("ggplot2")
  pred <- make_mock_mixgpd_predict("density")
  p <- plot_mixgpd_predict(pred)
  expect_true(inherits(p, "gg") || inherits(p, "ggplot"))
})

test_that("plot.mixgpd_predict works for survival", {
  skip_if_not_installed("ggplot2")
  pred <- make_mock_mixgpd_predict("survival")
  p <- plot_mixgpd_predict(pred)
  expect_true(inherits(p, "gg") || inherits(p, "ggplot"))
})

test_that("plot.mixgpd_predict works for location", {
  skip_if_not_installed("ggplot2")
  pred <- make_mock_mixgpd_predict("location")
  p <- plot_mixgpd_predict(pred)
  expect_true(inherits(p, "gg") || inherits(p, "ggplot") || is.list(p))
})

test_that("plot.mixgpd_predict errors on non-list input", {
  expect_error(plot_mixgpd_predict("not_a_list"), "must be a prediction object")
})

test_that("plot.mixgpd_predict errors on missing type", {
  pred <- list(fit = data.frame(x = 1))
  class(pred) <- "mixgpd_predict"
  expect_error(plot_mixgpd_predict(pred), "missing 'type'")
})

# ======================================================================
# print.causalmixgpd_causal_predict_plots tests
# ======================================================================

test_that("print.causalmixgpd_causal_predict_plots works", {
  skip_if_not_installed("ggplot2")
  plots <- make_mock_causal_predict_plots()
  expect_output(print_causalmixgpd_causal_predict_plots(plots), regexp = ".")
})

# ======================================================================
# print.mixgpd_predict_plots tests
# ======================================================================

test_that("print.mixgpd_predict_plots works", {
  skip_if_not_installed("ggplot2")
  p <- make_mock_mixgpd_predict_plots()
  # Should not error
  expect_silent(invisible(capture.output(print_mixgpd_predict_plots(p))))
})

# ======================================================================
# print.mixgpd_fit_plots tests
# ======================================================================

test_that("print.mixgpd_fit_plots works", {
  skip_if_not_installed("ggplot2")
  plots <- make_mock_mixgpd_fit_plots()
  expect_output(print_mixgpd_fit_plots(plots), "traceplot")
  expect_output(print_mixgpd_fit_plots(plots), "density")
})

# ======================================================================
# print.causalmixgpd_causal_fit_plots tests
# ======================================================================

test_that("print.causalmixgpd_causal_fit_plots works", {
  skip_if_not_installed("ggplot2")
  plots <- make_mock_causal_fit_plots()
  expect_output(print_causalmixgpd_causal_fit_plots(plots), "treated")
  expect_output(print_causalmixgpd_causal_fit_plots(plots), "control")
})

# ======================================================================
# plot.mixgpd_fitted tests
# ======================================================================

test_that("plot.mixgpd_fitted returns plot list", {
  skip_if_not_installed("ggplot2")
  fitted_obj <- make_mock_mixgpd_fitted()
  result <- plot_mixgpd_fitted(fitted_obj)
  expect_s3_class(result, "mixgpd_fitted_plots")
  expect_true("observed_fitted_plot" %in% names(result))
  expect_true("residual_plot" %in% names(result))
})

# ======================================================================
# print.mixgpd_fitted_plots tests
# ======================================================================

test_that("print.mixgpd_fitted_plots works", {
  skip_if_not_installed("ggplot2")
  plots <- make_mock_mixgpd_fitted_plots()
  # Should not error (print produces ggplot output but not text)
  expect_silent(invisible(capture.output(print_mixgpd_fitted_plots(plots))))
})

# ======================================================================
# plot.causalmixgpd_qte tests
# ======================================================================

test_that("plot.causalmixgpd_qte returns both plots by default", {
  skip_if_not_installed("ggplot2")
  qte <- make_mock_qte()
  result <- plot_causalmixgpd_qte(qte)
  expect_s3_class(result, "causalmixgpd_causal_predict_plots")
  expect_true("trt_control" %in% names(result))
  expect_true("treatment_effect" %in% names(result))
})

test_that("plot.causalmixgpd_qte defaults CQTE to effect plot faceted by id", {
  skip_if_not_installed("ggplot2")
  probs <- c(0.25, 0.5, 0.75)
  qte <- make_mock_qte()
  qte$type <- "cqte"
  qte$n_pred <- 3L
  qte$ps <- c(0.2, 0.5, 0.8)
  qte$probs <- probs
  qte$grid <- probs
  qte$fit <- matrix(c(
    0.1, 0.2, 0.3,
    0.2, 0.3, 0.4,
    0.3, 0.4, 0.5
  ), nrow = 3L, byrow = TRUE)
  qte$lower <- qte$fit - 0.1
  qte$upper <- qte$fit + 0.1

  result <- plot_causalmixgpd_qte(qte)
  expect_true(inherits(result, "gg") || inherits(result, "ggplot"))
  expect_true(is.factor(result$data$x_plot))
  expect_equal(levels(result$data$x_plot), c("0.25", "0.5", "0.75"))

  expect_warning(build <- ggplot2::ggplot_build(result), NA)
  line_data <- build$data[[1]]
  panel_unique_x <- vapply(split(line_data$x, line_data$PANEL), function(z) length(unique(z)), integer(1))

  expect_true(all(panel_unique_x == length(probs)))
  expect_equal(sort(unique(as.numeric(line_data$x))), seq_along(probs), tolerance = 1e-12)
})

test_that("plot.causalmixgpd_qte effect type works", {
  skip_if_not_installed("ggplot2")
  qte <- make_mock_qte()
  result <- plot_causalmixgpd_qte(qte, type = "effect")
  expect_true(inherits(result, "gg") || inherits(result, "ggplot"))
})

test_that("plot.causalmixgpd_qte marginal effect uses quantile levels on x-axis", {
  skip_if_not_installed("ggplot2")
  probs <- c(0.25, 0.5, 0.75)
  qte <- make_mock_qte()
  qte$n_pred <- 1L
  qte$ps <- NULL
  qte$probs <- probs
  qte$grid <- probs
  qte$fit <- matrix(c(0.1, 0.2, 0.3), nrow = 1L)
  qte$lower <- matrix(c(-0.1, 0.0, 0.1), nrow = 1L)
  qte$upper <- matrix(c(0.3, 0.4, 0.5), nrow = 1L)
  qte$qte$fit <- data.frame(
    id = 1L,
    index = probs,
    estimate = c(0.1, 0.2, 0.3),
    lower = c(-0.1, 0.0, 0.1),
    upper = c(0.3, 0.4, 0.5)
  )
  qte$trt$fit <- data.frame(id = 1L, index = probs, estimate = c(1.0, 1.3, 1.6))
  qte$con$fit <- data.frame(id = 1L, index = probs, estimate = c(0.9, 1.1, 1.3))

  result <- plot_causalmixgpd_qte(qte, type = "effect")
  expect_true(is.factor(result$data$x_plot))
  expect_equal(levels(result$data$x_plot), c("0.25", "0.5", "0.75"))
  build <- ggplot2::ggplot_build(result)
  line_data <- build$data[[1]]

  expect_equal(as.numeric(line_data$x), seq_along(probs), tolerance = 1e-12)
  expect_equal(length(unique(line_data$PANEL)), 1L)
})

test_that("plot.causalmixgpd_qte effect uses pointwise error bars instead of ribbons", {
  skip_if_not_installed("ggplot2")
  probs <- c(0.25, 0.5, 0.75)
  qte <- make_mock_qte()
  qte$n_pred <- 1L
  qte$ps <- NULL
  qte$probs <- probs
  qte$grid <- probs
  qte$fit <- matrix(c(0.1, 0.2, 0.3), nrow = 1L)
  qte$lower <- matrix(c(-0.1, 0.0, 0.1), nrow = 1L)
  qte$upper <- matrix(c(0.3, 0.4, 0.5), nrow = 1L)
  qte$trt$fit <- data.frame(id = 1L, index = probs, estimate = c(1.0, 1.3, 1.6))
  qte$con$fit <- data.frame(id = 1L, index = probs, estimate = c(0.9, 1.1, 1.3))

  result <- plot_causalmixgpd_qte(qte, type = "effect")
  geom_classes <- vapply(result[["layers"]], function(layer) class(layer[["geom"]])[1], character(1))

  expect_true("GeomErrorbar" %in% geom_classes)
  expect_false("GeomRibbon" %in% geom_classes)
})

test_that("plot.causalmixgpd_qte effect facets by id using quantile levels on x-axis", {
  skip_if_not_installed("ggplot2")
  probs <- c(0.25, 0.5, 0.75)
  qte <- make_mock_qte()
  qte$n_pred <- 3L
  qte$ps <- c(0.2, 0.5, 0.8)
  qte$probs <- probs
  qte$grid <- probs
  qte$fit <- matrix(c(
    0.1, 0.2, 0.3,
    0.2, 0.3, 0.4,
    0.3, 0.4, 0.5
  ), nrow = 3L, byrow = TRUE)
  qte$lower <- qte$fit - 0.1
  qte$upper <- qte$fit + 0.1

  result <- plot_causalmixgpd_qte(qte, type = "effect", facet_by = "id")
  expect_true(is.factor(result$data$x_plot))
  expect_equal(levels(result$data$x_plot), c("0.25", "0.5", "0.75"))
  expect_warning(build <- ggplot2::ggplot_build(result), NA)
  line_data <- build$data[[1]]
  panel_unique_x <- vapply(split(line_data$x, line_data$PANEL), function(z) length(unique(z)), integer(1))

  expect_true(all(panel_unique_x == length(probs)))
  expect_equal(sort(unique(as.numeric(line_data$x))), seq_along(probs), tolerance = 1e-12)
})

test_that("plot.causalmixgpd_qte arms type works", {
  skip_if_not_installed("ggplot2")
  qte <- make_mock_qte()
  result <- plot_causalmixgpd_qte(qte, type = "arms")
  expect_true(inherits(result, "gg") || inherits(result, "ggplot"))
})

test_that("plot.causalmixgpd_qte arms use pointwise error bars instead of ribbons", {
  skip_if_not_installed("ggplot2")
  probs <- c(0.25, 0.5, 0.75)
  qte <- make_mock_qte()
  qte$n_pred <- 1L
  qte$ps <- NULL
  qte$probs <- probs
  qte$grid <- probs
  qte$fit <- matrix(c(0.1, 0.2, 0.3), nrow = 1L)
  qte$lower <- matrix(c(-0.1, 0.0, 0.1), nrow = 1L)
  qte$upper <- matrix(c(0.3, 0.4, 0.5), nrow = 1L)
  qte$trt$fit <- data.frame(
    id = 1L,
    index = probs,
    estimate = c(1.0, 1.3, 1.6),
    lower = c(0.8, 1.1, 1.4),
    upper = c(1.2, 1.5, 1.8)
  )
  qte$con$fit <- data.frame(
    id = 1L,
    index = probs,
    estimate = c(0.9, 1.1, 1.3),
    lower = c(0.7, 0.9, 1.1),
    upper = c(1.1, 1.3, 1.5)
  )

  result <- plot_causalmixgpd_qte(qte, type = "arms")
  geom_classes <- vapply(result[["layers"]], function(layer) class(layer[["geom"]])[1], character(1))

  expect_true("GeomErrorbar" %in% geom_classes)
  expect_false("GeomRibbon" %in% geom_classes)
})

test_that("plot.causalmixgpd_qte arms facets by id using quantile levels on x-axis", {
  skip_if_not_installed("ggplot2")
  probs <- c(0.25, 0.5, 0.75)
  qte <- make_mock_qte()
  qte$n_pred <- 3L
  qte$ps <- c(0.2, 0.5, 0.8)
  qte$probs <- probs
  qte$grid <- probs
  qte$trt$fit <- data.frame(
    id = rep(1:3, each = 3),
    index = rep(probs, times = 3),
    estimate = c(1.0, 1.2, 1.4, 1.1, 1.3, 1.5, 1.2, 1.4, 1.6),
    lower = c(0.9, 1.1, 1.3, 1.0, 1.2, 1.4, 1.1, 1.3, 1.5),
    upper = c(1.1, 1.3, 1.5, 1.2, 1.4, 1.6, 1.3, 1.5, 1.7)
  )
  qte$con$fit <- data.frame(
    id = rep(1:3, each = 3),
    index = rep(probs, times = 3),
    estimate = c(0.8, 1.0, 1.2, 0.9, 1.1, 1.3, 1.0, 1.2, 1.4),
    lower = c(0.7, 0.9, 1.1, 0.8, 1.0, 1.2, 0.9, 1.1, 1.3),
    upper = c(0.9, 1.1, 1.3, 1.0, 1.2, 1.4, 1.1, 1.3, 1.5)
  )

  result <- plot_causalmixgpd_qte(qte, type = "arms", facet_by = "id")
  expect_true(is.factor(result$data$x_plot))
  expect_equal(levels(result$data$x_plot), c("0.25", "0.5", "0.75"))
  expect_warning(build <- ggplot2::ggplot_build(result), NA)
  line_data <- build$data[[1]]
  panel_unique_x <- vapply(split(line_data$x, line_data$PANEL), function(z) length(unique(z)), integer(1))

  expect_true(all(panel_unique_x == length(probs)))
  expect_equal(sort(unique(as.numeric(line_data$x))), seq_along(probs), tolerance = 1e-12)
})

test_that("plot.causalmixgpd_qte single quantile mentions tau in x-axis label", {
  skip_if_not_installed("ggplot2")
  qte <- make_mock_qte()
  qte$type <- "cqte"
  qte$probs <- 0.5
  qte$grid <- 0.5
  qte$n_pred <- 3L
  qte$ps <- c(0.2, 0.5, 0.8)
  qte$fit <- matrix(c(0.1, 0.2, 0.3), nrow = 3L, ncol = 1L)
  qte$lower <- qte$fit - 0.1
  qte$upper <- qte$fit + 0.1
  qte$trt$fit <- data.frame(id = 1:3, index = 0.5, estimate = c(1.0, 1.2, 1.4))
  qte$con$fit <- data.frame(id = 1:3, index = 0.5, estimate = c(0.9, 1.0, 1.1))

  result <- plot_causalmixgpd_qte(qte)
  expect_match(result$labels$x, "tau = 0.5", fixed = TRUE)
})

test_that("plot.causalmixgpd_qte applies categorical quantile axis and error bars to QTT", {
  skip_if_not_installed("ggplot2")
  probs <- c(0.25, 0.5, 0.75)
  qte <- make_mock_qte()
  qte$type <- "qtt"
  qte$n_pred <- 2L
  qte$ps <- c(0.3, 0.7)
  qte$probs <- probs
  qte$grid <- probs
  qte$fit <- matrix(c(0.1, 0.15, 0.2, 0.12, 0.18, 0.24), nrow = 2L, byrow = TRUE)
  qte$lower <- qte$fit - 0.05
  qte$upper <- qte$fit + 0.05
  qte$qte$fit <- data.frame(
    id = rep(1:2, each = 3),
    index = rep(probs, times = 2),
    estimate = c(0.1, 0.15, 0.2, 0.12, 0.18, 0.24),
    lower = c(0.05, 0.10, 0.15, 0.07, 0.13, 0.19),
    upper = c(0.15, 0.20, 0.25, 0.17, 0.23, 0.29)
  )

  result <- plot_causalmixgpd_qte(qte, type = "effect", facet_by = "id")
  geom_classes <- vapply(result$layers, function(layer) class(layer$geom)[1], character(1))

  expect_true(is.factor(result$data$x_plot))
  expect_equal(levels(result$data$x_plot), c("0.25", "0.5", "0.75"))
  expect_equal(result$labels$title, "QTT")
  expect_equal(result$labels$y, "Quantile Treatment Effect on the Treated")
  expect_true("GeomErrorbar" %in% geom_classes)
  expect_false("GeomRibbon" %in% geom_classes)
})

test_that("plot.causalmixgpd_qte keeps effect intervals aligned with estimates", {
  skip_if_not_installed("ggplot2")
  probs <- c(0.25, 0.5, 0.75)
  qte <- make_mock_qte()
  qte$type <- "cqte"
  qte$n_pred <- 2L
  qte$ps <- c(0.2, 0.8)
  qte$probs <- probs
  qte$grid <- probs
  qte$fit <- matrix(c(11, 12, 13, 21, 22, 23), nrow = 2L, byrow = TRUE)
  qte$lower <- matrix(c(101, 102, 103, 201, 202, 203), nrow = 2L, byrow = TRUE)
  qte$upper <- qte$lower + 0.5
  qte$qte$fit <- data.frame(
    id = rep(1:2, each = 3),
    index = rep(probs, times = 2),
    estimate = c(11, 12, 13, 21, 22, 23),
    lower = c(101, 102, 103, 201, 202, 203),
    upper = c(101.5, 102.5, 103.5, 201.5, 202.5, 203.5)
  )

  result <- plot_causalmixgpd_qte(qte, type = "effect", facet_by = "id")
  plotted <- result$data[order(result$data$id, result$data$index), c("id", "index", "estimate", "lower", "upper")]
  expected <- qte$qte$fit[order(qte$qte$fit$id, qte$qte$fit$index), c("id", "index", "estimate", "lower", "upper")]

  expect_equal(unname(as.matrix(plotted)), unname(as.matrix(expected)))
})

# ======================================================================
# plot.causalmixgpd_ate tests
# ======================================================================

test_that("plot.causalmixgpd_ate returns both plots by default", {
  skip_if_not_installed("ggplot2")
  ate <- make_mock_ate()
  result <- plot_causalmixgpd_ate(ate)
  expect_s3_class(result, "causalmixgpd_causal_predict_plots")
  expect_true("trt_control" %in% names(result))
  expect_true("treatment_effect" %in% names(result))
})

test_that("plot.causalmixgpd_ate defaults CATE to effect plot", {
  skip_if_not_installed("ggplot2")
  ate <- make_mock_ate()
  ate$type <- "cate"
  ate$ps <- c(0.2, 0.4, 0.6, 0.8, 0.9)
  ate$fit <- c(0.1, 0.15, 0.2, 0.25, 0.3)
  ate$lower <- ate$fit - 0.05
  ate$upper <- ate$fit + 0.05

  result <- plot_causalmixgpd_ate(ate)
  expect_true(inherits(result, "gg") || inherits(result, "ggplot"))
  expect_true(is.factor(result$data$x_plot))
  expect_equal(result$labels$x, "Index")
  expect_warning(ggplot2::ggplot_build(result), NA)
})

test_that("plot.causalmixgpd_ate uses profile labels for CATE when available", {
  skip_if_not_installed("ggplot2")
  ate <- make_mock_ate()
  ate$type <- "cate"
  ate$profile <- paste("Profile", 1:5)
  ate$ate$fit$profile <- ate$profile

  result <- plot_causalmixgpd_ate(ate)
  expect_true(is.factor(result$data$x_plot))
  expect_equal(levels(result$data$x_plot), ate$profile)
  expect_equal(result$labels$x, "Profile")
  expect_warning(ggplot2::ggplot_build(result), NA)
})

test_that("plot.causalmixgpd_ate effect type works", {
  skip_if_not_installed("ggplot2")
  ate <- make_mock_ate()
  result <- plot_causalmixgpd_ate(ate, type = "effect")
  expect_true(inherits(result, "gg") || inherits(result, "ggplot"))
})

test_that("plot.causalmixgpd_ate marginal effect returns a single point", {
  skip_if_not_installed("ggplot2")
  ate <- make_mock_ate()
  ate$n_pred <- 1L
  ate$ps <- NULL
  ate$fit <- 0.25
  ate$lower <- -0.10
  ate$upper <- 0.45
  ate$ate$fit <- data.frame(id = 1L, estimate = 0.25, lower = -0.10, upper = 0.45)
  ate$trt$fit <- data.frame(id = 1L, estimate = 1.20, lower = 1.00, upper = 1.35)
  ate$con$fit <- data.frame(id = 1L, estimate = 0.95, lower = 0.80, upper = 1.10)

  result <- plot_causalmixgpd_ate(ate, type = "effect")
  build <- ggplot2::ggplot_build(result)

  expect_equal(nrow(build$data[[1]]), 1L)
  expect_equal(as.numeric(build$data[[1]]$y), 0.25, tolerance = 1e-12)
})

test_that("plot.causalmixgpd_ate uses pointwise error bars instead of ribbons", {
  skip_if_not_installed("ggplot2")
  ate <- make_mock_ate()
  ate$type <- "cate"
  ate$ps <- c(0.2, 0.4, 0.6, 0.8, 0.9)
  ate$fit <- c(0.1, 0.15, 0.2, 0.25, 0.3)
  ate$lower <- ate$fit - 0.05
  ate$upper <- ate$fit + 0.05
  ate$trt$fit <- data.frame(
    id = 1:5,
    estimate = c(1.0, 1.1, 1.2, 1.3, 1.4),
    lower = c(0.9, 1.0, 1.1, 1.2, 1.3),
    upper = c(1.1, 1.2, 1.3, 1.4, 1.5)
  )
  ate$con$fit <- data.frame(
    id = 1:5,
    estimate = c(0.9, 0.95, 1.0, 1.05, 1.1),
    lower = c(0.8, 0.85, 0.9, 0.95, 1.0),
    upper = c(1.0, 1.05, 1.1, 1.15, 1.2)
  )

  effect_plot <- plot_causalmixgpd_ate(ate, type = "effect")
  effect_geoms <- vapply(effect_plot[["layers"]], function(layer) class(layer[["geom"]])[1], character(1))
  expect_true("GeomErrorbar" %in% effect_geoms)
  expect_false("GeomRibbon" %in% effect_geoms)

  arms_plot <- plot_causalmixgpd_ate(ate, type = "arms")
  arms_geoms <- vapply(arms_plot[["layers"]], function(layer) class(layer[["geom"]])[1], character(1))
  expect_true("GeomErrorbar" %in% arms_geoms)
  expect_false("GeomRibbon" %in% arms_geoms)
})

test_that("plot.causalmixgpd_ate applies shared effect styling to ATT", {
  skip_if_not_installed("ggplot2")
  ate <- make_mock_ate()
  ate$type <- "att"
  ate$ps <- c(0.25, 0.5, 0.75)
  ate$fit <- c(0.1, 0.18, 0.26)
  ate$lower <- ate$fit - 0.04
  ate$upper <- ate$fit + 0.04

  result <- plot_causalmixgpd_ate(ate, type = "effect")
  geom_classes <- vapply(result$layers, function(layer) class(layer$geom)[1], character(1))

  expect_equal(result$labels$title, "ATT")
  expect_equal(result$labels$y, "Average Treatment Effect on the Treated")
  expect_true("GeomErrorbar" %in% geom_classes)
  expect_false("GeomRibbon" %in% geom_classes)
})

test_that("plot.causalmixgpd_ate arms type works", {
  skip_if_not_installed("ggplot2")
  ate <- make_mock_ate()
  result <- plot_causalmixgpd_ate(ate, type = "arms")
  expect_true(inherits(result, "gg") || inherits(result, "ggplot"))
})

# ======================================================================
# Error handling
# ======================================================================

test_that("plot.causalmixgpd_qte errors on invalid object", {
  skip_if_not_installed("ggplot2")
  invalid <- list(x = 1)
  class(invalid) <- "causalmixgpd_qte"
  expect_error(plot_causalmixgpd_qte(invalid), "Invalid QTE object")
})

test_that("plot.causalmixgpd_ate errors on invalid object", {
  skip_if_not_installed("ggplot2")
  invalid <- list(x = 1)
  class(invalid) <- "causalmixgpd_ate"
  expect_error(plot_causalmixgpd_ate(invalid), "Invalid ATE object")
})

# ======================================================================
# Additional S3 method tests for bundles and fits
# ======================================================================

pkg <- "CausalMixGPD"
print_causalmixgpd_bundle <- getFromNamespace("print.causalmixgpd_bundle", pkg)
print_causalmixgpd_causal_bundle <- getFromNamespace("print.causalmixgpd_causal_bundle", pkg)
summary_causalmixgpd_bundle <- getFromNamespace("summary.causalmixgpd_bundle", pkg)
summary_causalmixgpd_causal_bundle <- getFromNamespace("summary.causalmixgpd_causal_bundle", pkg)
print_causalmixgpd_ps_bundle <- getFromNamespace("print.causalmixgpd_ps_bundle", pkg)
summary_causalmixgpd_ps_bundle <- getFromNamespace("summary.causalmixgpd_ps_bundle", pkg)
print_causalmixgpd_causal_fit <- getFromNamespace("print.causalmixgpd_causal_fit", pkg)
summary_causalmixgpd_causal_fit <- getFromNamespace("summary.causalmixgpd_causal_fit", pkg)
print_causalmixgpd_ps_fit <- getFromNamespace("print.causalmixgpd_ps_fit", pkg)
summary_causalmixgpd_ps_fit <- getFromNamespace("summary.causalmixgpd_ps_fit", pkg)
print_mixgpd_fit <- getFromNamespace("print.mixgpd_fit", pkg)
summary_mixgpd_fit <- getFromNamespace("summary.mixgpd_fit", pkg)
print_mixgpd_summary <- getFromNamespace("print.mixgpd_summary", pkg)

# ======================================================================
# print/summary.causalmixgpd_bundle tests
# ======================================================================

test_that("print.causalmixgpd_bundle works with basic bundle", {
  set.seed(1)
  y <- abs(rnorm(20)) + 0.1
  bundle <- build_nimble_bundle(
    y = y,
    backend = "sb",
    kernel = "normal",
    GPD = FALSE,
    components = 4
  )
  expect_output(print_causalmixgpd_bundle(bundle), "CausalMixGPD bundle")
  expect_output(print_causalmixgpd_bundle(bundle), "Stick-Breaking")
  expect_output(print_causalmixgpd_bundle(bundle), "Normal")
})

test_that("print.causalmixgpd_bundle shows code when requested", {
  set.seed(1)
  y <- abs(rnorm(20)) + 0.1
  bundle <- build_nimble_bundle(
    y = y,
    backend = "sb",
    kernel = "normal",
    GPD = FALSE,
    components = 4
  )
  expect_output(print_causalmixgpd_bundle(bundle, code = TRUE), "Model code")
})

test_that("summary.causalmixgpd_bundle works", {
  set.seed(1)
  y <- abs(rnorm(20)) + 0.1
  bundle <- build_nimble_bundle(
    y = y,
    backend = "sb",
    kernel = "normal",
    GPD = FALSE,
    components = 4
  )
  expect_output(summary_causalmixgpd_bundle(bundle), "CausalMixGPD bundle summary")
  expect_output(summary_causalmixgpd_bundle(bundle), "Parameter specification")
  expect_output(summary_causalmixgpd_bundle(bundle), "Monitors")
})

# ======================================================================
# print/summary.causalmixgpd_causal_bundle tests
# ======================================================================

test_that("print.causalmixgpd_causal_bundle works", {
  set.seed(1)
  y <- rnorm(20)
  X <- matrix(rnorm(40), ncol = 2)
  A <- c(rep(0, 10), rep(1, 10))

  bundle <- build_causal_bundle(
    y = y, X = X, A = A,
    backend = "sb", kernel = "normal",
    GPD = FALSE, components = 4
  )
  expect_output(print_causalmixgpd_causal_bundle(bundle), "CausalMixGPD causal bundle")
  expect_output(print_causalmixgpd_causal_bundle(bundle), "Outcome.*treated")
  expect_output(print_causalmixgpd_causal_bundle(bundle), "Outcome.*control")
})

test_that("print.causalmixgpd_causal_bundle shows PS info for observational", {
  set.seed(1)
  y <- rnorm(20)
  X <- matrix(rnorm(40), ncol = 2)
  A <- c(rep(0, 10), rep(1, 10))

  bundle <- build_causal_bundle(
    y = y, X = X, A = A,
    backend = "sb", kernel = "normal",
    GPD = FALSE, components = 4, PS = "logit"
  )
  expect_output(print_causalmixgpd_causal_bundle(bundle), "PS model.*logit")
})

test_that("summary.causalmixgpd_causal_bundle works", {
  set.seed(1)
  y <- rnorm(20)
  X <- matrix(rnorm(40), ncol = 2)
  A <- c(rep(0, 10), rep(1, 10))

  bundle <- build_causal_bundle(
    y = y, X = X, A = A,
    backend = "sb", kernel = "normal",
    GPD = FALSE, components = 4
  )
  expect_output(summary_causalmixgpd_causal_bundle(bundle), "CausalMixGPD causal bundle summary")
})

# ======================================================================
# Mock PS bundle tests
# ======================================================================

make_mock_ps_bundle <- function() {
  out <- list(
    spec = list(
      meta = list(
        type = "ps_logit",
        include_intercept = TRUE
      )
    ),
    code = quote({
      beta[1] ~ dnorm(0, 1)
    }),
    data = list(X = cbind(`(Intercept)` = 1, x1 = c(-1, 0, 1, 2))),
    monitors = "beta"
  )
  class(out) <- "causalmixgpd_ps_bundle"
  out
}

test_that("print.causalmixgpd_ps_bundle works", {
  bundle <- make_mock_ps_bundle()
  expect_output(print_causalmixgpd_ps_bundle(bundle), "PS bundle")
  expect_output(print_causalmixgpd_ps_bundle(bundle), "model: logit")
})

test_that("print.causalmixgpd_ps_bundle shows code when requested", {
  bundle <- make_mock_ps_bundle()
  expect_output(print_causalmixgpd_ps_bundle(bundle, code = TRUE), "beta")
})

test_that("summary.causalmixgpd_ps_bundle works", {
  bundle <- make_mock_ps_bundle()
  expect_output(summary_causalmixgpd_ps_bundle(bundle), "PS bundle")
})

# ======================================================================
# Mock PS fit tests
# ======================================================================

make_mock_ps_fit <- function() {
  out <- list(
    bundle = make_mock_ps_bundle(),
    mcmc = list(
      samples = matrix(
        c(-0.5, 0.2,
          -0.1, 0.4,
          0.2, 0.6,
          0.5, 0.8),
        ncol = 2,
        byrow = TRUE,
        dimnames = list(NULL, c("beta[1]", "beta[2]"))
      )
    )
  )
  class(out) <- "causalmixgpd_ps_fit"
  out
}

test_that("print.causalmixgpd_ps_fit works", {
  fit <- make_mock_ps_fit()
  expect_output(print_causalmixgpd_ps_fit(fit), "CausalMixGPD PS fit")
  expect_output(print_causalmixgpd_ps_fit(fit), "model: logit")
})

test_that("summary.causalmixgpd_ps_fit works", {
  fit <- make_mock_ps_fit()
  summ <- summary_causalmixgpd_ps_fit(fit)
  expect_s3_class(summ, "summary.causalmixgpd_ps_fit")
  expect_true(all(c("parameter", "mean", "sd", "ess") %in% names(summ$table)))
  expect_output(print_summary_causalmixgpd_ps_fit(summ), "CausalMixGPD PS fit summary")
  expect_output(print_summary_causalmixgpd_ps_fit(summ), "Summary table")
  expect_false(any(grepl("\\bess\\b", capture.output(print_summary_causalmixgpd_ps_fit(summ)))))
  expect_true(any(grepl("\\bess\\b", capture.output(print_summary_causalmixgpd_ps_fit(summ, show_ess = TRUE)))))
})

test_that("params.causalmixgpd_ps_fit returns named beta values", {
  fit <- make_mock_ps_fit()
  out <- params(fit)
  expect_s3_class(out, "mixgpd_params")
  expect_true("beta" %in% names(out))
  expect_equal(names(out$beta), c("(Intercept)", "x1"))
  expect_equal(unname(out$beta), c(0.025, 0.5), tolerance = 1e-8)
})

test_that("print.mixgpd_summary hides ess by default", {
  summ <- structure(
    list(
      model = list(
        backend = "sb",
        kernel = "normal",
        gpd = FALSE,
        epsilon = 0.025,
        truncation = list(Kt = 2L),
        n = 20L,
        components = 3L
      ),
      waic = NULL,
      table = data.frame(
        parameter = c("alpha", "weights[1]"),
        mean = c(1.2, 0.7),
        sd = c(0.3, 0.1),
        ess = c(42, 55)
      )
    ),
    class = "mixgpd_summary"
  )

  expect_false(any(grepl("\\bess\\b", capture.output(print.mixgpd_summary(summ)))))
  expect_true(any(grepl("\\bess\\b", capture.output(print.mixgpd_summary(summ, show_ess = TRUE)))))
})

# ======================================================================
# Mock causal fit tests
# ======================================================================

make_mock_mixgpd_fit <- function() {
  out <- list(
    spec = list(
      meta = list(
        backend = "sb",
        kernel = "normal",
        GPD = FALSE,
        N = 50,
        components = 4
      ),
      dispatch = list(backend = "sb")
    ),
    data = list(y = rnorm(50)),
    mcmc = list(niter = 100, nburnin = 20, thin = 1, nchains = 1),
    epsilon = 0.025
  )
  class(out) <- "mixgpd_fit"
  out
}

make_mock_causal_fit <- function() {
  out <- list(
    bundle = list(
      meta = list(
        backend = list(trt = "sb", con = "sb"),
        kernel = list(trt = "normal", con = "normal"),
        GPD = list(trt = FALSE, con = FALSE),
        ps = list(enabled = FALSE, model_type = FALSE)
      ),
      data = list(X = NULL)
    ),
    outcome_fit = list(
      trt = make_mock_mixgpd_fit(),
      con = make_mock_mixgpd_fit()
    ),
    ps_fit = NULL
  )
  class(out) <- "causalmixgpd_causal_fit"
  out
}

test_that("print.causalmixgpd_causal_fit works", {
  fit <- make_mock_causal_fit()
  expect_output(print_causalmixgpd_causal_fit(fit), "CausalMixGPD causal fit")
  expect_output(print_causalmixgpd_causal_fit(fit), "Outcome.*treated")
  expect_output(print_causalmixgpd_causal_fit(fit), "Outcome.*control")
})

test_that("print.causalmixgpd_causal_fit shows PS info", {
  fit <- make_mock_causal_fit()
  expect_output(print_causalmixgpd_causal_fit(fit), "PS model")
})

test_that("summary.causalmixgpd_causal_fit returns arm summaries", {
  fit <- make_mock_causal_fit()
  fit$bundle$data$X <- cbind(x1 = c(-1, 1))
  fit$bundle$meta$ps$enabled <- TRUE
  fit$ps_fit <- make_mock_ps_fit()

  testthat::local_mocked_bindings(
    summary.mixgpd_fit = function(object, pars = NULL, probs = c(0.025, 0.5, 0.975), ...) {
      out <- list(
        model = list(
          backend = "sb",
          kernel = object$spec$meta$kernel,
          gpd = FALSE,
          n = 50,
          components = 4,
          epsilon = 0.025,
          truncation = list(Kt = 2)
        ),
        waic = list(WAIC = 123.4, lppd = -60.1, pWAIC = 1.6),
        table = data.frame(
          parameter = paste0(c("w[1]", "w[2]", "mean[1]", "mean[2]"), "_", object$spec$meta$kernel),
          mean = c(0.6, 0.4, 1.5, 2.5),
          sd = c(0.1, 0.1, 0.2, 0.3)
        )
      )
      class(out) <- "mixgpd_summary"
      out
    },
    .package = "CausalMixGPD"
  )

  summ <- summary_causalmixgpd_causal_fit(fit)
  expect_s3_class(summ, "summary.causalmixgpd_causal_fit")
  expect_s3_class(summ$ps, "summary.causalmixgpd_ps_fit")
  expect_s3_class(summ$outcome$control, "mixgpd_summary")
  expect_s3_class(summ$outcome$treated, "mixgpd_summary")
  expect_output(print_summary_causalmixgpd_causal_fit(summ), "Outcome fits")
  expect_output(print_summary_causalmixgpd_causal_fit(summ), "\\[control\\]")
  expect_output(print_summary_causalmixgpd_causal_fit(summ), "MixGPD summary")
})

test_that("params.causalmixgpd_causal_fit includes PS block when available", {
  fit <- make_mock_causal_fit()
  fit$bundle$data$X <- cbind(x1 = c(-1, 1))
  fit$bundle$meta$ps$enabled <- TRUE
  fit$ps_fit <- make_mock_ps_fit()

  testthat::local_mocked_bindings(
    params.mixgpd_fit = function(object, ...) {
      structure(list(alpha = 1, w = c(0.7, 0.3)), class = "mixgpd_params")
    },
    .package = "CausalMixGPD"
  )

  out <- params(fit)
  expect_s3_class(out, "mixgpd_params_pair")
  expect_true(all(c("ps", "treated", "control") %in% names(out)))
  expect_equal(names(out$ps$beta), c("(Intercept)", "x1"))
  expect_output(print_mixgpd_params_pair(out), "\\[ps\\]")
})

# ======================================================================
# Mock mixgpd_fit print tests
# ======================================================================

test_that("print.mixgpd_fit works", {
  fit <- make_mock_mixgpd_fit()
  expect_output(print_mixgpd_fit(fit), "MixGPD fit")
  expect_output(print_mixgpd_fit(fit), "Stick-Breaking")
  expect_output(print_mixgpd_fit(fit), "Normal")
})

# ======================================================================
# Mock summary output tests
# ======================================================================

make_mock_mixgpd_summary <- function() {
  out <- list(
    model = list(
      backend = "sb",
      kernel = "normal",
      gpd = FALSE,
      n = 50,
      components = 4,
      epsilon = 0.025,
      truncation = list(Kt = 2)
    ),
    waic = list(WAIC = 123.4, lppd = -60.1, pWAIC = 1.6),
    table = data.frame(
      parameter = c("w[1]", "w[2]", "mean[1]", "mean[2]"),
      mean = c(0.6, 0.4, 1.5, 2.5),
      sd = c(0.1, 0.1, 0.2, 0.3)
    )
  )
  class(out) <- "mixgpd_summary"
  out
}

test_that("print.mixgpd_summary works", {
  summ <- make_mock_mixgpd_summary()
  expect_output(print_mixgpd_summary(summ), "MixGPD summary")
  expect_output(print_mixgpd_summary(summ), "Stick-Breaking")
  expect_output(print_mixgpd_summary(summ), "Normal")
})

# ===== END unit/test-s3-methods.R =====

# ===== BEGIN unit/test-theory.R =====
test_that("GPD splice is continuous at the threshold", {
  w <- c(0.3, 0.7)
  mean <- c(-1, 1)
  sd <- c(0.5, 1)
  threshold <- 0.2
  tail_scale <- 1.0
  tail_shape <- 0.2

  p_bulk <- pNormMix(threshold, w = w, mean = mean, sd = sd,
                     lower.tail = TRUE, log.p = FALSE)
  p_splice <- pNormMixGpd(threshold, w = w, mean = mean, sd = sd,
                          threshold = threshold, tail_scale = tail_scale,
                          tail_shape = tail_shape, lower.tail = TRUE, log.p = FALSE)
  expect_equal(p_splice, p_bulk, tolerance = 1e-6)
})

test_that("Tail dominates at high quantiles with GPD", {
  w <- c(0.3, 0.7)
  mean <- c(-1, 1)
  sd <- c(0.5, 1)
  threshold <- 0.2
  # Make the tail appreciably heavier so the GPD splice should inflate extremes
  tail_scale <- 2.5
  tail_shape <- 0.5

  q_bulk <- qNormMix(0.99, w = w, mean = mean, sd = sd)
  q_tail <- qNormMixGpd(0.99, w = w, mean = mean, sd = sd,
                        threshold = threshold, tail_scale = tail_scale,
                        tail_shape = tail_shape)
  expect_gt(q_tail, q_bulk)
})

test_that("SB and CRP predictions agree in a small synthetic case", {
  skip_if_not_test_level("ci")  # MCMC tests are slow, skip at cran level

  set.seed(42)
  y <- abs(stats::rnorm(20)) + 0.1
  mcmc_cfg <- list(niter = 40, nburnin = 10, thin = 1, nchains = 1, seed = 1)

  sb_bundle <- build_nimble_bundle(
    y = y,
    backend = "sb",
    kernel = "normal",
    GPD = FALSE,
    components = 6,
    mcmc = mcmc_cfg
  )
  crp_bundle <- build_nimble_bundle(
    y = y,
    backend = "crp",
    kernel = "normal",
    GPD = FALSE,
    components = 6,
    mcmc = mcmc_cfg
  )

  sb_fit <- run_mcmc_bundle_manual(sb_bundle, show_progress = FALSE)
  crp_fit <- run_mcmc_bundle_manual(crp_bundle, show_progress = FALSE)

  q_sb <- predict(sb_fit, type = "quantile", index = 0.5)$fit[1, 1]
  q_crp <- predict(crp_fit, type = "quantile", index = 0.5)$fit[1, 1]
  expect_lt(abs(q_sb - q_crp), 1.0)
})

test_that("GPD on/off changes high-quantile behavior", {
  skip_if_not_test_level("ci")  # MCMC tests are slow, skip at cran level

  set.seed(123)
  y <- abs(stats::rnorm(20)) + 0.1
  mcmc_cfg <- list(niter = 40, nburnin = 10, thin = 1, nchains = 1, seed = 2)

  fit_off <- run_mcmc_bundle_manual(build_nimble_bundle(
    y = y,
    backend = "sb",
    kernel = "normal",
    GPD = FALSE,
    components = 6,
    mcmc = mcmc_cfg
  ), show_progress = FALSE)

  fit_on <- run_mcmc_bundle_manual(build_nimble_bundle(
    y = y,
    backend = "sb",
    kernel = "normal",
    GPD = TRUE,
    components = 6,
    mcmc = mcmc_cfg
  ), show_progress = FALSE)

  q_off <- predict(fit_off, type = "quantile", index = 0.99)$fit[1, 1]
  q_on <- predict(fit_on, type = "quantile", index = 0.99)$fit[1, 1]
  pr_on <- params(fit_on)
  ts <- as.numeric(pr_on$tail_scale %||% NA_real_)[1]
  tsh <- as.numeric(pr_on$tail_shape %||% NA_real_)[1]
  if (!is.finite(ts) || !is.finite(tsh) || abs(ts) < 1e-6) {
    skip("Tail parameters not identified in short MCMC; skip GPD effect check.")
  }
  # Turning on the GPD tail should alter high-quantile predictions when tail parameters move.
  expect_gt(abs(q_on - q_off), 0.1)
})

# ===== END unit/test-theory.R =====

# ===== BEGIN unit/test-vectorization.R =====
test_that("lowercase d/p/q functions vectorize over first argument", {

  w <- c(0.60, 0.40)

  shape <- c(2.0, 5.0)
  scale <- c(1.0, 2.0)
  x <- seq(0.5, 2.0, length.out = 5)

  # Use lowercase vectorized wrappers
  d_vec <- dgammamix(x, w = w, shape = shape, scale = scale, log = FALSE)
  d_scalar <- vapply(x, function(xx) {
    as.numeric(dGammaMix(xx, w = w, shape = shape, scale = scale, log = 0L))
  }, numeric(1))
  expect_equal(d_vec, d_scalar, tolerance = 1e-10)

  p_vec <- pgammamix(q = x, w = w, shape = shape, scale = scale,
                     lower.tail = TRUE, log.p = FALSE)
  p_scalar <- vapply(x, function(xx) {
    as.numeric(pGammaMix(xx, w = w, shape = shape, scale = scale,
              lower.tail = 1L, log.p = 0L))
  }, numeric(1))
  expect_equal(p_vec, p_scalar, tolerance = 1e-10)

  probs <- c(0.1, 0.5, 0.9)
  q_vec <- qgammamix(probs, w = w, shape = shape, scale = scale)
  q_scalar <- vapply(probs, function(pp) {
    qGammaMix(pp, w = w, shape = shape, scale = scale)
  }, numeric(1))
  expect_equal(q_vec, q_scalar, tolerance = 1e-10)
})

test_that("lowercase r functions support n > 1", {

  w <- c(0.60, 0.40)
  shape <- c(2.0, 5.0)
  scale <- c(1.0, 2.0)

  set.seed(123)
  draws <- rgammamix(10, w = w, shape = shape, scale = scale)
  expect_length(draws, 10)
  expect_true(all(is.finite(draws)))
})

test_that("lowercase base wrappers vectorize over first argument", {

  x <- c(1.2, 1.5, 2.0)
  probs <- c(0.2, 0.5, 0.9)

  threshold <- 1.0
  scale <- 0.8
  shape <- 0.2

  expect_equal(
    dgpd(x, threshold = threshold, scale = scale, shape = shape, log = FALSE),
    vapply(x, function(xx) as.numeric(dGpd(xx, threshold, scale, shape, 0L)), numeric(1))
  )
  expect_equal(
    pgpd(x, threshold = threshold, scale = scale, shape = shape,
         lower.tail = TRUE, log.p = FALSE),
    vapply(x, function(xx) as.numeric(pGpd(xx, threshold, scale, shape, 1L, 0L)), numeric(1))
  )
  expect_equal(
    qgpd(probs, threshold = threshold, scale = scale, shape = shape,
         lower.tail = TRUE, log.p = FALSE),
    vapply(probs, function(pp) qGpd(pp, threshold, scale, shape, lower.tail = TRUE, log.p = FALSE),
           numeric(1))
  )

  mean <- 2.0
  shape_ig <- 3.0

  expect_equal(
    dinvgauss(x, mean = mean, shape = shape_ig, log = FALSE),
    vapply(x, function(xx) as.numeric(dInvGauss(xx, mean, shape_ig, 0L)), numeric(1))
  )
  expect_equal(
    pinvgauss(x, mean = mean, shape = shape_ig, lower.tail = TRUE, log.p = FALSE),
    vapply(x, function(xx) as.numeric(pInvGauss(xx, mean, shape_ig, 1L, 0L)), numeric(1))
  )
  expect_equal(
    qinvgauss(probs, mean = mean, shape = shape_ig, lower.tail = TRUE, log.p = FALSE),
    vapply(probs, function(pp) qInvGauss(pp, mean, shape_ig, lower.tail = TRUE, log.p = FALSE),
           numeric(1))
  )

  loc <- 0.0
  scale_am <- 1.2
  shape1 <- 2.0
  shape2 <- 1.5

  expect_equal(
    damoroso(x, loc = loc, scale = scale_am, shape1 = shape1, shape2 = shape2, log = FALSE),
    vapply(x, function(xx) as.numeric(dAmoroso(xx, loc, scale_am, shape1, shape2, 0L)), numeric(1))
  )
  expect_equal(
    pamoroso(x, loc = loc, scale = scale_am, shape1 = shape1, shape2 = shape2,
             lower.tail = TRUE, log.p = FALSE),
    vapply(x, function(xx) as.numeric(pAmoroso(xx, loc, scale_am, shape1, shape2, 1L, 0L)),
           numeric(1))
  )
  expect_equal(
    qamoroso(probs, loc = loc, scale = scale_am, shape1 = shape1, shape2 = shape2,
             lower.tail = TRUE, log.p = FALSE),
    vapply(probs, function(pp) qAmoroso(pp, loc, scale_am, shape1, shape2,
                                        lower.tail = TRUE, log.p = FALSE),
           numeric(1))
  )

  location <- 0.0
  scale_c <- 1.1

  expect_equal(
    dcauchy_vec(x, location = location, scale = scale_c, log = FALSE),
    vapply(x, function(xx) as.numeric(dCauchy(xx, location, scale_c, 0L)), numeric(1))
  )
  expect_equal(
    pcauchy_vec(x, location = location, scale = scale_c, lower.tail = TRUE, log.p = FALSE),
    vapply(x, function(xx) as.numeric(pCauchy(xx, location, scale_c, 1L, 0L)), numeric(1))
  )
  expect_equal(
    qcauchy_vec(probs, location = location, scale = scale_c, lower.tail = TRUE, log.p = FALSE),
    vapply(probs, function(pp) qCauchy(pp, location, scale_c, lower.tail = TRUE, log.p = FALSE),
           numeric(1))
  )
})

test_that("lowercase base r wrappers support n > 1", {

  set.seed(456)
  draws_gpd <- rgpd(5, threshold = 1.0, scale = 0.8, shape = 0.2)
  expect_length(draws_gpd, 5)
  expect_true(all(is.finite(draws_gpd)))

  draws_ig <- rinvgauss(5, mean = 2.0, shape = 3.0)
  expect_length(draws_ig, 5)
  expect_true(all(is.finite(draws_ig)))

  draws_am <- ramoroso(5, loc = 0.0, scale = 1.2, shape1 = 2.0, shape2 = 1.5)
  expect_length(draws_am, 5)
  expect_true(all(is.finite(draws_am)))

  draws_c <- rcauchy_vec(5, location = 0.0, scale = 1.1)
  expect_length(draws_c, 5)
  expect_true(all(is.finite(draws_c)))
})

test_that("lowercase mix wrappers vectorize over first argument", {

  x <- c(-1.0, 0.5, 1.2)
  probs <- c(0.2, 0.5, 0.9)

  w <- c(0.7, 0.3)
  location <- c(-0.5, 0.8)
  scale <- c(1.0, 1.5)

  expect_equal(
    dcauchymix(x, w = w, location = location, scale = scale, log = FALSE),
    vapply(x, function(xx) as.numeric(dCauchyMix(xx, w = w, location = location, scale = scale, log = 0L)),
           numeric(1))
  )
  expect_equal(
    pcauchymix(x, w = w, location = location, scale = scale, lower.tail = TRUE, log.p = FALSE),
    vapply(x, function(xx) as.numeric(pCauchyMix(xx, w = w, location = location, scale = scale,
                                                 lower.tail = 1L, log.p = 0L)),
           numeric(1))
  )
  expect_equal(
    qcauchymix(probs, w = w, location = location, scale = scale, lower.tail = TRUE, log.p = FALSE),
    vapply(probs, function(pp) qCauchyMix(pp, w = w, location = location, scale = scale,
                                          lower.tail = TRUE, log.p = FALSE),
           numeric(1))
  )
})

test_that("lowercase mixgpd and gpd wrappers vectorize over first argument", {

  x <- c(-0.5, 0.5, 2.5)
  probs <- c(0.1, 0.5, 0.9)

  w <- c(0.6, 0.4)
  mean <- c(0.0, 1.5)
  sd <- c(1.0, 1.8)
  threshold <- 1.2
  tail_scale <- 1.0
  tail_shape <- 0.15

  expect_equal(
    dnormmixgpd(x, w = w, mean = mean, sd = sd, threshold = threshold,
                tail_scale = tail_scale, tail_shape = tail_shape, log = FALSE),
    vapply(x, function(xx) as.numeric(dNormMixGpd(xx, w = w, mean = mean, sd = sd,
                                                 threshold = threshold, tail_scale = tail_scale,
                                                 tail_shape = tail_shape, log = 0L)),
           numeric(1))
  )
  expect_equal(
    pnormmixgpd(x, w = w, mean = mean, sd = sd, threshold = threshold,
                tail_scale = tail_scale, tail_shape = tail_shape,
                lower.tail = TRUE, log.p = FALSE),
    vapply(x, function(xx) as.numeric(pNormMixGpd(xx, w = w, mean = mean, sd = sd,
                                                 threshold = threshold, tail_scale = tail_scale,
                                                 tail_shape = tail_shape, lower.tail = 1L, log.p = 0L)),
           numeric(1))
  )
  expect_equal(
    qnormmixgpd(probs, w = w, mean = mean, sd = sd, threshold = threshold,
                tail_scale = tail_scale, tail_shape = tail_shape,
                lower.tail = TRUE, log.p = FALSE),
    vapply(probs, function(pp) qNormMixGpd(pp, w = w, mean = mean, sd = sd,
                                          threshold = threshold, tail_scale = tail_scale,
                                          tail_shape = tail_shape, lower.tail = TRUE, log.p = FALSE),
           numeric(1))
  )

  expect_equal(
    dnormgpd(x, mean = mean[1], sd = sd[1], threshold = threshold,
             tail_scale = tail_scale, tail_shape = tail_shape, log = FALSE),
    vapply(x, function(xx) as.numeric(dNormGpd(xx, mean = mean[1], sd = sd[1],
                                              threshold = threshold, tail_scale = tail_scale,
                                              tail_shape = tail_shape, log = 0L)),
           numeric(1))
  )
  expect_equal(
    pnormgpd(x, mean = mean[1], sd = sd[1], threshold = threshold,
             tail_scale = tail_scale, tail_shape = tail_shape,
             lower.tail = TRUE, log.p = FALSE),
    vapply(x, function(xx) as.numeric(pNormGpd(xx, mean = mean[1], sd = sd[1],
                                              threshold = threshold, tail_scale = tail_scale,
                                              tail_shape = tail_shape, lower.tail = 1L, log.p = 0L)),
           numeric(1))
  )
  expect_equal(
    qnormgpd(probs, mean = mean[1], sd = sd[1], threshold = threshold,
             tail_scale = tail_scale, tail_shape = tail_shape,
             lower.tail = TRUE, log.p = FALSE),
    vapply(probs, function(pp) qNormGpd(pp, mean = mean[1], sd = sd[1],
                                        threshold = threshold, tail_scale = tail_scale,
                                        tail_shape = tail_shape, lower.tail = TRUE, log.p = FALSE),
           numeric(1))
  )
})

test_that("lowercase gamma mixgpd and gpd wrappers vectorize over first argument", {
  x <- c(1.0, 2.2, 4.5)
  probs <- c(0.2, 0.6, 0.9)

  w <- c(0.6, 0.4)
  shape <- c(2.0, 4.0)
  scale <- c(1.0, 2.0)
  threshold <- 2.5
  tail_scale <- 0.9
  tail_shape <- 0.2

  expect_equal(
    dgammamixgpd(x, w = w, shape = shape, scale = scale, threshold = threshold,
                 tail_scale = tail_scale, tail_shape = tail_shape, log = FALSE),
    vapply(x, function(xx) as.numeric(dGammaMixGpd(xx, w = w, shape = shape, scale = scale,
                                                  threshold = threshold, tail_scale = tail_scale,
                                                  tail_shape = tail_shape, log = 0L)),
           numeric(1))
  )
  expect_equal(
    pgammamixgpd(x, w = w, shape = shape, scale = scale, threshold = threshold,
                 tail_scale = tail_scale, tail_shape = tail_shape,
                 lower.tail = TRUE, log.p = FALSE),
    vapply(x, function(xx) as.numeric(pGammaMixGpd(xx, w = w, shape = shape, scale = scale,
                                                  threshold = threshold, tail_scale = tail_scale,
                                                  tail_shape = tail_shape, lower.tail = 1L, log.p = 0L)),
           numeric(1))
  )
  expect_equal(
    qgammamixgpd(probs, w = w, shape = shape, scale = scale, threshold = threshold,
                 tail_scale = tail_scale, tail_shape = tail_shape,
                 lower.tail = TRUE, log.p = FALSE),
    vapply(probs, function(pp) qGammaMixGpd(pp, w = w, shape = shape, scale = scale,
                                           threshold = threshold, tail_scale = tail_scale,
                                           tail_shape = tail_shape, lower.tail = TRUE, log.p = FALSE),
           numeric(1))
  )

  expect_equal(
    dgammagpd(x, shape = shape[1], scale = scale[1], threshold = threshold,
              tail_scale = tail_scale, tail_shape = tail_shape, log = FALSE),
    vapply(x, function(xx) as.numeric(dGammaGpd(xx, shape = shape[1], scale = scale[1],
                                               threshold = threshold, tail_scale = tail_scale,
                                               tail_shape = tail_shape, log = 0L)),
           numeric(1))
  )
  expect_equal(
    pgammagpd(x, shape = shape[1], scale = scale[1], threshold = threshold,
              tail_scale = tail_scale, tail_shape = tail_shape,
              lower.tail = TRUE, log.p = FALSE),
    vapply(x, function(xx) as.numeric(pGammaGpd(xx, shape = shape[1], scale = scale[1],
                                               threshold = threshold, tail_scale = tail_scale,
                                               tail_shape = tail_shape, lower.tail = 1L, log.p = 0L)),
           numeric(1))
  )
  expect_equal(
    qgammagpd(probs, shape = shape[1], scale = scale[1], threshold = threshold,
              tail_scale = tail_scale, tail_shape = tail_shape,
              lower.tail = TRUE, log.p = FALSE),
    vapply(probs, function(pp) qGammaGpd(pp, shape = shape[1], scale = scale[1],
                                        threshold = threshold, tail_scale = tail_scale,
                                        tail_shape = tail_shape, lower.tail = TRUE, log.p = FALSE),
           numeric(1))
  )
})

test_that("lowercase laplace wrappers vectorize over first argument", {
  x <- c(-1.0, 0.5, 2.0)
  probs <- c(0.2, 0.6, 0.9)

  w <- c(0.7, 0.3)
  location <- c(-0.5, 1.0)
  scale <- c(1.0, 1.4)
  threshold <- 0.8
  tail_scale <- 0.9
  tail_shape <- 0.15

  expect_equal(
    dlaplacemix(x, w = w, location = location, scale = scale, log = FALSE),
    vapply(x, function(xx) as.numeric(dLaplaceMix(xx, w = w, location = location, scale = scale, log = 0L)),
           numeric(1))
  )
  expect_equal(
    plaplacemix(x, w = w, location = location, scale = scale, lower.tail = TRUE, log.p = FALSE),
    vapply(x, function(xx) as.numeric(pLaplaceMix(xx, w = w, location = location, scale = scale,
                                                 lower.tail = 1L, log.p = 0L)),
           numeric(1))
  )
  expect_equal(
    qlaplacemix(probs, w = w, location = location, scale = scale,
                lower.tail = TRUE, log.p = FALSE),
    vapply(probs, function(pp) qLaplaceMix(pp, w = w, location = location, scale = scale,
                                          lower.tail = TRUE, log.p = FALSE),
           numeric(1))
  )

  expect_equal(
    dlaplacemixgpd(x, w = w, location = location, scale = scale, threshold = threshold,
                   tail_scale = tail_scale, tail_shape = tail_shape, log = FALSE),
    vapply(x, function(xx) as.numeric(dLaplaceMixGpd(xx, w = w, location = location, scale = scale,
                                                    threshold = threshold, tail_scale = tail_scale,
                                                    tail_shape = tail_shape, log = 0L)),
           numeric(1))
  )
  expect_equal(
    plaplacemixgpd(x, w = w, location = location, scale = scale, threshold = threshold,
                   tail_scale = tail_scale, tail_shape = tail_shape,
                   lower.tail = TRUE, log.p = FALSE),
    vapply(x, function(xx) as.numeric(pLaplaceMixGpd(xx, w = w, location = location, scale = scale,
                                                    threshold = threshold, tail_scale = tail_scale,
                                                    tail_shape = tail_shape, lower.tail = 1L, log.p = 0L)),
           numeric(1))
  )
  expect_equal(
    qlaplacemixgpd(probs, w = w, location = location, scale = scale, threshold = threshold,
                   tail_scale = tail_scale, tail_shape = tail_shape,
                   lower.tail = TRUE, log.p = FALSE),
    vapply(probs, function(pp) qLaplaceMixGpd(pp, w = w, location = location, scale = scale,
                                             threshold = threshold, tail_scale = tail_scale,
                                             tail_shape = tail_shape, lower.tail = TRUE, log.p = FALSE),
           numeric(1))
  )

  expect_equal(
    dlaplacegpd(x, location = location[1], scale = scale[1], threshold = threshold,
               tail_scale = tail_scale, tail_shape = tail_shape, log = FALSE),
    vapply(x, function(xx) as.numeric(dLaplaceGpd(xx, location = location[1], scale = scale[1],
                                                 threshold = threshold, tail_scale = tail_scale,
                                                 tail_shape = tail_shape, log = 0L)),
           numeric(1))
  )
  expect_equal(
    plaplacegpd(x, location = location[1], scale = scale[1], threshold = threshold,
               tail_scale = tail_scale, tail_shape = tail_shape,
               lower.tail = TRUE, log.p = FALSE),
    vapply(x, function(xx) as.numeric(pLaplaceGpd(xx, location = location[1], scale = scale[1],
                                                 threshold = threshold, tail_scale = tail_scale,
                                                 tail_shape = tail_shape, lower.tail = 1L, log.p = 0L)),
           numeric(1))
  )
  expect_equal(
    qlaplacegpd(probs, location = location[1], scale = scale[1], threshold = threshold,
               tail_scale = tail_scale, tail_shape = tail_shape,
               lower.tail = TRUE, log.p = FALSE),
    vapply(probs, function(pp) qLaplaceGpd(pp, location = location[1], scale = scale[1],
                                          threshold = threshold, tail_scale = tail_scale,
                                          tail_shape = tail_shape, lower.tail = TRUE, log.p = FALSE),
           numeric(1))
  )
})

test_that("lowercase lognormal wrappers vectorize over first argument", {
  x <- c(0.5, 1.2, 3.0)
  probs <- c(0.2, 0.6, 0.9)

  w <- c(0.7, 0.3)
  meanlog <- c(0.0, 0.4)
  sdlog <- c(0.3, 0.6)
  threshold <- 1.8
  tail_scale <- 0.8
  tail_shape <- 0.1

  expect_equal(
    dlognormalmix(x, w = w, meanlog = meanlog, sdlog = sdlog, log = FALSE),
    vapply(x, function(xx) as.numeric(dLognormalMix(xx, w = w, meanlog = meanlog, sdlog = sdlog, log = 0L)),
           numeric(1))
  )
  expect_equal(
    plognormalmix(x, w = w, meanlog = meanlog, sdlog = sdlog, lower.tail = TRUE, log.p = FALSE),
    vapply(x, function(xx) as.numeric(pLognormalMix(xx, w = w, meanlog = meanlog, sdlog = sdlog,
                                                   lower.tail = 1L, log.p = 0L)),
           numeric(1))
  )
  expect_equal(
    qlognormalmix(probs, w = w, meanlog = meanlog, sdlog = sdlog,
                  lower.tail = TRUE, log.p = FALSE),
    vapply(probs, function(pp) qLognormalMix(pp, w = w, meanlog = meanlog, sdlog = sdlog,
                                            lower.tail = TRUE, log.p = FALSE),
           numeric(1))
  )

  expect_equal(
    dlognormalmixgpd(x, w = w, meanlog = meanlog, sdlog = sdlog, threshold = threshold,
                     tail_scale = tail_scale, tail_shape = tail_shape, log = FALSE),
    vapply(x, function(xx) as.numeric(dLognormalMixGpd(xx, w = w, meanlog = meanlog, sdlog = sdlog,
                                                      threshold = threshold, tail_scale = tail_scale,
                                                      tail_shape = tail_shape, log = 0L)),
           numeric(1))
  )
  expect_equal(
    plognormalmixgpd(x, w = w, meanlog = meanlog, sdlog = sdlog, threshold = threshold,
                     tail_scale = tail_scale, tail_shape = tail_shape,
                     lower.tail = TRUE, log.p = FALSE),
    vapply(x, function(xx) as.numeric(pLognormalMixGpd(xx, w = w, meanlog = meanlog, sdlog = sdlog,
                                                      threshold = threshold, tail_scale = tail_scale,
                                                      tail_shape = tail_shape, lower.tail = 1L, log.p = 0L)),
           numeric(1))
  )
  expect_equal(
    qlognormalmixgpd(probs, w = w, meanlog = meanlog, sdlog = sdlog, threshold = threshold,
                     tail_scale = tail_scale, tail_shape = tail_shape,
                     lower.tail = TRUE, log.p = FALSE),
    vapply(probs, function(pp) qLognormalMixGpd(pp, w = w, meanlog = meanlog, sdlog = sdlog,
                                               threshold = threshold, tail_scale = tail_scale,
                                               tail_shape = tail_shape, lower.tail = TRUE, log.p = FALSE),
           numeric(1))
  )

  expect_equal(
    dlognormalgpd(x, meanlog = meanlog[1], sdlog = sdlog[1], threshold = threshold,
                  tail_scale = tail_scale, tail_shape = tail_shape, log = FALSE),
    vapply(x, function(xx) as.numeric(dLognormalGpd(xx, meanlog = meanlog[1], sdlog = sdlog[1],
                                                   threshold = threshold, tail_scale = tail_scale,
                                                   tail_shape = tail_shape, log = 0L)),
           numeric(1))
  )
  expect_equal(
    plognormalgpd(x, meanlog = meanlog[1], sdlog = sdlog[1], threshold = threshold,
                  tail_scale = tail_scale, tail_shape = tail_shape,
                  lower.tail = TRUE, log.p = FALSE),
    vapply(x, function(xx) as.numeric(pLognormalGpd(xx, meanlog = meanlog[1], sdlog = sdlog[1],
                                                   threshold = threshold, tail_scale = tail_scale,
                                                   tail_shape = tail_shape, lower.tail = 1L, log.p = 0L)),
           numeric(1))
  )
  expect_equal(
    qlognormalgpd(probs, meanlog = meanlog[1], sdlog = sdlog[1], threshold = threshold,
                  tail_scale = tail_scale, tail_shape = tail_shape,
                  lower.tail = TRUE, log.p = FALSE),
    vapply(probs, function(pp) qLognormalGpd(pp, meanlog = meanlog[1], sdlog = sdlog[1],
                                            threshold = threshold, tail_scale = tail_scale,
                                            tail_shape = tail_shape, lower.tail = TRUE, log.p = FALSE),
           numeric(1))
  )
})

test_that("lowercase invgauss wrappers vectorize over first argument", {
  x <- c(0.8, 1.5, 3.0)
  probs <- c(0.2, 0.6, 0.9)

  w <- c(0.6, 0.4)
  mean <- c(1.2, 2.0)
  shape <- c(2.5, 4.0)
  threshold <- 1.6
  tail_scale <- 0.7
  tail_shape <- 0.15

  expect_equal(
    dinvgaussmix(x, w = w, mean = mean, shape = shape, log = FALSE),
    vapply(x, function(xx) as.numeric(dInvGaussMix(xx, w = w, mean = mean, shape = shape, log = 0L)),
           numeric(1))
  )
  expect_equal(
    pinvgaussmix(x, w = w, mean = mean, shape = shape, lower.tail = TRUE, log.p = FALSE),
    vapply(x, function(xx) as.numeric(pInvGaussMix(xx, w = w, mean = mean, shape = shape,
                                                  lower.tail = 1L, log.p = 0L)),
           numeric(1))
  )
  expect_equal(
    qinvgaussmix(probs, w = w, mean = mean, shape = shape,
                 lower.tail = TRUE, log.p = FALSE),
    vapply(probs, function(pp) qInvGaussMix(pp, w = w, mean = mean, shape = shape,
                                           lower.tail = TRUE, log.p = FALSE),
           numeric(1))
  )

  expect_equal(
    dinvgaussmixgpd(x, w = w, mean = mean, shape = shape, threshold = threshold,
                    tail_scale = tail_scale, tail_shape = tail_shape, log = FALSE),
    vapply(x, function(xx) as.numeric(dInvGaussMixGpd(xx, w = w, mean = mean, shape = shape,
                                                     threshold = threshold, tail_scale = tail_scale,
                                                     tail_shape = tail_shape, log = 0L)),
           numeric(1))
  )
  expect_equal(
    pinvgaussmixgpd(x, w = w, mean = mean, shape = shape, threshold = threshold,
                    tail_scale = tail_scale, tail_shape = tail_shape,
                    lower.tail = TRUE, log.p = FALSE),
    vapply(x, function(xx) as.numeric(pInvGaussMixGpd(xx, w = w, mean = mean, shape = shape,
                                                     threshold = threshold, tail_scale = tail_scale,
                                                     tail_shape = tail_shape, lower.tail = 1L, log.p = 0L)),
           numeric(1))
  )
  expect_equal(
    qinvgaussmixgpd(probs, w = w, mean = mean, shape = shape, threshold = threshold,
                    tail_scale = tail_scale, tail_shape = tail_shape,
                    lower.tail = TRUE, log.p = FALSE),
    vapply(probs, function(pp) qInvGaussMixGpd(pp, w = w, mean = mean, shape = shape,
                                              threshold = threshold, tail_scale = tail_scale,
                                              tail_shape = tail_shape, lower.tail = TRUE, log.p = FALSE),
           numeric(1))
  )

  expect_equal(
    dinvgaussgpd(x, mean = mean[1], shape = shape[1], threshold = threshold,
                 tail_scale = tail_scale, tail_shape = tail_shape, log = FALSE),
    vapply(x, function(xx) as.numeric(dInvGaussGpd(xx, mean = mean[1], shape = shape[1],
                                                  threshold = threshold, tail_scale = tail_scale,
                                                  tail_shape = tail_shape, log = 0L)),
           numeric(1))
  )
  expect_equal(
    pinvgaussgpd(x, mean = mean[1], shape = shape[1], threshold = threshold,
                 tail_scale = tail_scale, tail_shape = tail_shape,
                 lower.tail = TRUE, log.p = FALSE),
    vapply(x, function(xx) as.numeric(pInvGaussGpd(xx, mean = mean[1], shape = shape[1],
                                                  threshold = threshold, tail_scale = tail_scale,
                                                  tail_shape = tail_shape, lower.tail = 1L, log.p = 0L)),
           numeric(1))
  )
  expect_equal(
    qinvgaussgpd(probs, mean = mean[1], shape = shape[1], threshold = threshold,
                 tail_scale = tail_scale, tail_shape = tail_shape,
                 lower.tail = TRUE, log.p = FALSE),
    vapply(probs, function(pp) qInvGaussGpd(pp, mean = mean[1], shape = shape[1],
                                           threshold = threshold, tail_scale = tail_scale,
                                           tail_shape = tail_shape, lower.tail = TRUE, log.p = FALSE),
           numeric(1))
  )
})

test_that("lowercase amoroso wrappers vectorize over first argument", {
  x <- c(0.8, 1.4, 3.0)
  probs <- c(0.2, 0.6, 0.9)

  w <- c(0.5, 0.3, 0.2)
  loc <- c(0.0, 0.5, 1.0)
  scale <- c(1.0, 1.2, 1.5)
  shape1 <- c(2.0, 3.0, 4.0)
  shape2 <- c(1.1, 1.2, 1.3)
  threshold <- 1.6
  tail_scale <- 0.8
  tail_shape <- 0.15

  expect_equal(
    damorosomix(x, w = w, loc = loc, scale = scale, shape1 = shape1, shape2 = shape2, log = FALSE),
    vapply(x, function(xx) as.numeric(dAmorosoMix(xx, w = w, loc = loc, scale = scale,
                                                 shape1 = shape1, shape2 = shape2, log = 0L)),
           numeric(1))
  )
  expect_equal(
    pamorosomix(x, w = w, loc = loc, scale = scale, shape1 = shape1, shape2 = shape2,
                lower.tail = TRUE, log.p = FALSE),
    vapply(x, function(xx) as.numeric(pAmorosoMix(xx, w = w, loc = loc, scale = scale,
                                                 shape1 = shape1, shape2 = shape2,
                                                 lower.tail = 1L, log.p = 0L)),
           numeric(1))
  )
  expect_equal(
    qamorosomix(probs, w = w, loc = loc, scale = scale, shape1 = shape1, shape2 = shape2,
                lower.tail = TRUE, log.p = FALSE),
    vapply(probs, function(pp) qAmorosoMix(pp, w = w, loc = loc, scale = scale,
                                          shape1 = shape1, shape2 = shape2,
                                          lower.tail = TRUE, log.p = FALSE),
           numeric(1))
  )

  expect_equal(
    damorosomixgpd(x, w = w, loc = loc, scale = scale, shape1 = shape1, shape2 = shape2,
                   threshold = threshold, tail_scale = tail_scale, tail_shape = tail_shape, log = FALSE),
    vapply(x, function(xx) as.numeric(dAmorosoMixGpd(xx, w = w, loc = loc, scale = scale,
                                                    shape1 = shape1, shape2 = shape2,
                                                    threshold = threshold, tail_scale = tail_scale,
                                                    tail_shape = tail_shape, log = 0L)),
           numeric(1))
  )
  expect_equal(
    pamorosomixgpd(x, w = w, loc = loc, scale = scale, shape1 = shape1, shape2 = shape2,
                   threshold = threshold, tail_scale = tail_scale, tail_shape = tail_shape,
                   lower.tail = TRUE, log.p = FALSE),
    vapply(x, function(xx) as.numeric(pAmorosoMixGpd(xx, w = w, loc = loc, scale = scale,
                                                    shape1 = shape1, shape2 = shape2,
                                                    threshold = threshold, tail_scale = tail_scale,
                                                    tail_shape = tail_shape, lower.tail = 1L, log.p = 0L)),
           numeric(1))
  )
  expect_equal(
    qamorosomixgpd(probs, w = w, loc = loc, scale = scale, shape1 = shape1, shape2 = shape2,
                   threshold = threshold, tail_scale = tail_scale, tail_shape = tail_shape,
                   lower.tail = TRUE, log.p = FALSE),
    vapply(probs, function(pp) qAmorosoMixGpd(pp, w = w, loc = loc, scale = scale,
                                             shape1 = shape1, shape2 = shape2,
                                             threshold = threshold, tail_scale = tail_scale,
                                             tail_shape = tail_shape, lower.tail = TRUE, log.p = FALSE),
           numeric(1))
  )

  expect_equal(
    damorosogpd(x, loc = loc[1], scale = scale[1], shape1 = shape1[1], shape2 = shape2[1],
                threshold = threshold, tail_scale = tail_scale, tail_shape = tail_shape, log = FALSE),
    vapply(x, function(xx) as.numeric(dAmorosoGpd(xx, loc = loc[1], scale = scale[1],
                                                 shape1 = shape1[1], shape2 = shape2[1],
                                                 threshold = threshold, tail_scale = tail_scale,
                                                 tail_shape = tail_shape, log = 0L)),
           numeric(1))
  )
  expect_equal(
    pamorosogpd(x, loc = loc[1], scale = scale[1], shape1 = shape1[1], shape2 = shape2[1],
                threshold = threshold, tail_scale = tail_scale, tail_shape = tail_shape,
                lower.tail = TRUE, log.p = FALSE),
    vapply(x, function(xx) as.numeric(pAmorosoGpd(xx, loc = loc[1], scale = scale[1],
                                                 shape1 = shape1[1], shape2 = shape2[1],
                                                 threshold = threshold, tail_scale = tail_scale,
                                                 tail_shape = tail_shape, lower.tail = 1L, log.p = 0L)),
           numeric(1))
  )
  expect_equal(
    qamorosogpd(probs, loc = loc[1], scale = scale[1], shape1 = shape1[1], shape2 = shape2[1],
                threshold = threshold, tail_scale = tail_scale, tail_shape = tail_shape,
                lower.tail = TRUE, log.p = FALSE),
    vapply(probs, function(pp) qAmorosoGpd(pp, loc = loc[1], scale = scale[1],
                                          shape1 = shape1[1], shape2 = shape2[1],
                                          threshold = threshold, tail_scale = tail_scale,
                                          tail_shape = tail_shape, lower.tail = TRUE, log.p = FALSE),
           numeric(1))
  )
})

test_that("lowercase normal mix wrappers vectorize over first argument", {
  x <- c(-1.0, 0.2, 1.5)
  probs <- c(0.2, 0.6, 0.9)

  w <- c(0.7, 0.3)
  mean <- c(0.0, 1.2)
  sd <- c(1.0, 1.6)

  expect_equal(
    dnormmix(x, w = w, mean = mean, sd = sd, log = FALSE),
    vapply(x, function(xx) as.numeric(dNormMix(xx, w = w, mean = mean, sd = sd, log = 0L)),
           numeric(1))
  )
  expect_equal(
    pnormmix(x, w = w, mean = mean, sd = sd, lower.tail = TRUE, log.p = FALSE),
    vapply(x, function(xx) as.numeric(pNormMix(xx, w = w, mean = mean, sd = sd,
                                              lower.tail = 1L, log.p = 0L)),
           numeric(1))
  )
  expect_equal(
    qnormmix(probs, w = w, mean = mean, sd = sd,
             lower.tail = TRUE, log.p = FALSE),
    vapply(probs, function(pp) qNormMix(pp, w = w, mean = mean, sd = sd,
                                       lower.tail = TRUE, log.p = FALSE),
           numeric(1))
  )
})

test_that("lowercase mixgpd and gpd r wrappers support n > 1", {

  w <- c(0.6, 0.4)
  mean <- c(0.0, 1.5)
  sd <- c(1.0, 1.8)
  threshold <- 1.2
  tail_scale <- 1.0
  tail_shape <- 0.15

  set.seed(789)
  draws_mixgpd <- rnormmixgpd(7, w = w, mean = mean, sd = sd, threshold = threshold,
                             tail_scale = tail_scale, tail_shape = tail_shape)
  expect_length(draws_mixgpd, 7)
  expect_true(all(is.finite(draws_mixgpd)))

  draws_gpd <- rnormgpd(7, mean = mean[1], sd = sd[1], threshold = threshold,
                        tail_scale = tail_scale, tail_shape = tail_shape)
  expect_length(draws_gpd, 7)
  expect_true(all(is.finite(draws_gpd)))
})

test_that("vectorized d/p/q options match scalar versions", {
  d_specs <- list(
    list(vec = dgpd, scalar = dGpd, x = c(1.2, 1.6, 2.0),
         args = list(threshold = 1.0, scale = 0.8, shape = 0.2)),
    list(vec = dnormmix, scalar = dNormMix, x = c(-1.0, 0.5, 1.2),
         args = list(w = c(0.7, 0.3), mean = c(0.0, 1.2), sd = c(1.0, 1.6))),
    list(vec = dnormmixgpd, scalar = dNormMixGpd, x = c(-0.5, 0.5, 2.5),
         args = list(w = c(0.6, 0.4), mean = c(0.0, 1.5), sd = c(1.0, 1.8),
                     threshold = 1.2, tail_scale = 1.0, tail_shape = 0.15)),
    list(vec = dnormgpd, scalar = dNormGpd, x = c(-0.5, 0.5, 2.5),
         args = list(mean = 0.0, sd = 1.0, threshold = 1.2, tail_scale = 1.0, tail_shape = 0.15)),
    list(vec = dgammamix, scalar = dGammaMix, x = c(1.0, 2.2, 4.5),
         args = list(w = c(0.6, 0.4), shape = c(2.0, 4.0), scale = c(1.0, 2.0))),
    list(vec = dgammamixgpd, scalar = dGammaMixGpd, x = c(1.0, 2.2, 4.5),
         args = list(w = c(0.6, 0.4), shape = c(2.0, 4.0), scale = c(1.0, 2.0),
                     threshold = 2.5, tail_scale = 0.9, tail_shape = 0.2)),
    list(vec = dgammagpd, scalar = dGammaGpd, x = c(1.0, 2.2, 4.5),
         args = list(shape = 2.0, scale = 1.0, threshold = 2.5, tail_scale = 0.9, tail_shape = 0.2)),
    list(vec = dlognormalmix, scalar = dLognormalMix, x = c(0.5, 1.2, 3.0),
         args = list(w = c(0.7, 0.3), meanlog = c(0.0, 0.4), sdlog = c(0.3, 0.6))),
    list(vec = dlognormalmixgpd, scalar = dLognormalMixGpd, x = c(0.5, 1.2, 3.0),
         args = list(w = c(0.7, 0.3), meanlog = c(0.0, 0.4), sdlog = c(0.3, 0.6),
                     threshold = 1.8, tail_scale = 0.8, tail_shape = 0.1)),
    list(vec = dlognormalgpd, scalar = dLognormalGpd, x = c(0.5, 1.2, 3.0),
         args = list(meanlog = 0.0, sdlog = 0.3, threshold = 1.8, tail_scale = 0.8, tail_shape = 0.1)),
    list(vec = dlaplacemix, scalar = dLaplaceMix, x = c(-1.0, 0.5, 2.0),
         args = list(w = c(0.7, 0.3), location = c(-0.5, 1.0), scale = c(1.0, 1.4))),
    list(vec = dlaplacemixgpd, scalar = dLaplaceMixGpd, x = c(-1.0, 0.5, 2.0),
         args = list(w = c(0.7, 0.3), location = c(-0.5, 1.0), scale = c(1.0, 1.4),
                     threshold = 0.8, tail_scale = 0.9, tail_shape = 0.15)),
    list(vec = dlaplacegpd, scalar = dLaplaceGpd, x = c(-1.0, 0.5, 2.0),
         args = list(location = -0.5, scale = 1.0, threshold = 0.8, tail_scale = 0.9, tail_shape = 0.15)),
    list(vec = dinvgaussmix, scalar = dInvGaussMix, x = c(0.8, 1.5, 3.0),
         args = list(w = c(0.6, 0.4), mean = c(1.2, 2.0), shape = c(2.5, 4.0))),
    list(vec = dinvgaussmixgpd, scalar = dInvGaussMixGpd, x = c(0.8, 1.5, 3.0),
         args = list(w = c(0.6, 0.4), mean = c(1.2, 2.0), shape = c(2.5, 4.0),
                     threshold = 1.6, tail_scale = 0.7, tail_shape = 0.15)),
    list(vec = dinvgaussgpd, scalar = dInvGaussGpd, x = c(0.8, 1.5, 3.0),
         args = list(mean = 1.2, shape = 2.5, threshold = 1.6, tail_scale = 0.7, tail_shape = 0.15)),
    list(vec = dinvgauss, scalar = dInvGauss, x = c(1.2, 1.5, 2.0),
         args = list(mean = 2.0, shape = 3.0)),
    list(vec = damorosomix, scalar = dAmorosoMix, x = c(0.8, 1.4, 3.0),
         args = list(w = c(0.5, 0.3, 0.2), loc = c(0.0, 0.5, 1.0), scale = c(1.0, 1.2, 1.5),
                     shape1 = c(2.0, 3.0, 4.0), shape2 = c(1.1, 1.2, 1.3))),
    list(vec = damorosomixgpd, scalar = dAmorosoMixGpd, x = c(0.8, 1.4, 3.0),
         args = list(w = c(0.5, 0.3, 0.2), loc = c(0.0, 0.5, 1.0), scale = c(1.0, 1.2, 1.5),
                     shape1 = c(2.0, 3.0, 4.0), shape2 = c(1.1, 1.2, 1.3),
                     threshold = 1.6, tail_scale = 0.8, tail_shape = 0.15)),
    list(vec = damorosogpd, scalar = dAmorosoGpd, x = c(0.8, 1.4, 3.0),
         args = list(loc = 0.0, scale = 1.0, shape1 = 2.0, shape2 = 1.1,
                     threshold = 1.6, tail_scale = 0.8, tail_shape = 0.15)),
    list(vec = damoroso, scalar = dAmoroso, x = c(1.2, 1.5, 2.0),
         args = list(loc = 0.0, scale = 1.2, shape1 = 2.0, shape2 = 1.5)),
    list(vec = dcauchy_vec, scalar = dCauchy, x = c(-1.0, 0.0, 1.0),
         args = list(location = 0.0, scale = 1.1)),
    list(vec = dcauchymix, scalar = dCauchyMix, x = c(-1.0, 0.5, 1.2),
         args = list(w = c(0.7, 0.3), location = c(-0.5, 0.8), scale = c(1.0, 1.5)))
  )

  for (spec in d_specs) {
    vec <- do.call(spec$vec, c(list(spec$x), spec$args, list(log = TRUE)))
    scalar <- vapply(
      spec$x,
      function(xx) as.numeric(do.call(spec$scalar, c(list(xx), spec$args, list(log = 1L)))),
      numeric(1)
    )
    expect_equal(vec, scalar, tolerance = 1e-10)
  }

  p_specs <- list(
    list(vec = pgpd, scalar = pGpd, q = c(1.2, 1.6, 2.0),
         args = list(threshold = 1.0, scale = 0.8, shape = 0.2)),
    list(vec = pnormmix, scalar = pNormMix, q = c(-1.0, 0.5, 1.2),
         args = list(w = c(0.7, 0.3), mean = c(0.0, 1.2), sd = c(1.0, 1.6))),
    list(vec = pnormmixgpd, scalar = pNormMixGpd, q = c(-0.5, 0.5, 2.5),
         args = list(w = c(0.6, 0.4), mean = c(0.0, 1.5), sd = c(1.0, 1.8),
                     threshold = 1.2, tail_scale = 1.0, tail_shape = 0.15)),
    list(vec = pnormgpd, scalar = pNormGpd, q = c(-0.5, 0.5, 2.5),
         args = list(mean = 0.0, sd = 1.0, threshold = 1.2, tail_scale = 1.0, tail_shape = 0.15)),
    list(vec = pgammamix, scalar = pGammaMix, q = c(1.0, 2.2, 4.5),
         args = list(w = c(0.6, 0.4), shape = c(2.0, 4.0), scale = c(1.0, 2.0))),
    list(vec = pgammamixgpd, scalar = pGammaMixGpd, q = c(1.0, 2.2, 4.5),
         args = list(w = c(0.6, 0.4), shape = c(2.0, 4.0), scale = c(1.0, 2.0),
                     threshold = 2.5, tail_scale = 0.9, tail_shape = 0.2)),
    list(vec = pgammagpd, scalar = pGammaGpd, q = c(1.0, 2.2, 4.5),
         args = list(shape = 2.0, scale = 1.0, threshold = 2.5, tail_scale = 0.9, tail_shape = 0.2)),
    list(vec = plognormalmix, scalar = pLognormalMix, q = c(0.5, 1.2, 3.0),
         args = list(w = c(0.7, 0.3), meanlog = c(0.0, 0.4), sdlog = c(0.3, 0.6))),
    list(vec = plognormalmixgpd, scalar = pLognormalMixGpd, q = c(0.5, 1.2, 3.0),
         args = list(w = c(0.7, 0.3), meanlog = c(0.0, 0.4), sdlog = c(0.3, 0.6),
                     threshold = 1.8, tail_scale = 0.8, tail_shape = 0.1)),
    list(vec = plognormalgpd, scalar = pLognormalGpd, q = c(0.5, 1.2, 3.0),
         args = list(meanlog = 0.0, sdlog = 0.3, threshold = 1.8, tail_scale = 0.8, tail_shape = 0.1)),
    list(vec = plaplacemix, scalar = pLaplaceMix, q = c(-1.0, 0.5, 2.0),
         args = list(w = c(0.7, 0.3), location = c(-0.5, 1.0), scale = c(1.0, 1.4))),
    list(vec = plaplacemixgpd, scalar = pLaplaceMixGpd, q = c(-1.0, 0.5, 2.0),
         args = list(w = c(0.7, 0.3), location = c(-0.5, 1.0), scale = c(1.0, 1.4),
                     threshold = 0.8, tail_scale = 0.9, tail_shape = 0.15)),
    list(vec = plaplacegpd, scalar = pLaplaceGpd, q = c(-1.0, 0.5, 2.0),
         args = list(location = -0.5, scale = 1.0, threshold = 0.8, tail_scale = 0.9, tail_shape = 0.15)),
    list(vec = pinvgaussmix, scalar = pInvGaussMix, q = c(0.8, 1.5, 3.0),
         args = list(w = c(0.6, 0.4), mean = c(1.2, 2.0), shape = c(2.5, 4.0))),
    list(vec = pinvgaussmixgpd, scalar = pInvGaussMixGpd, q = c(0.8, 1.5, 3.0),
         args = list(w = c(0.6, 0.4), mean = c(1.2, 2.0), shape = c(2.5, 4.0),
                     threshold = 1.6, tail_scale = 0.7, tail_shape = 0.15)),
    list(vec = pinvgaussgpd, scalar = pInvGaussGpd, q = c(0.8, 1.5, 3.0),
         args = list(mean = 1.2, shape = 2.5, threshold = 1.6, tail_scale = 0.7, tail_shape = 0.15)),
    list(vec = pinvgauss, scalar = pInvGauss, q = c(1.2, 1.5, 2.0),
         args = list(mean = 2.0, shape = 3.0)),
    list(vec = pamorosomix, scalar = pAmorosoMix, q = c(0.8, 1.4, 3.0),
         args = list(w = c(0.5, 0.3, 0.2), loc = c(0.0, 0.5, 1.0), scale = c(1.0, 1.2, 1.5),
                     shape1 = c(2.0, 3.0, 4.0), shape2 = c(1.1, 1.2, 1.3))),
    list(vec = pamorosomixgpd, scalar = pAmorosoMixGpd, q = c(0.8, 1.4, 3.0),
         args = list(w = c(0.5, 0.3, 0.2), loc = c(0.0, 0.5, 1.0), scale = c(1.0, 1.2, 1.5),
                     shape1 = c(2.0, 3.0, 4.0), shape2 = c(1.1, 1.2, 1.3),
                     threshold = 1.6, tail_scale = 0.8, tail_shape = 0.15)),
    list(vec = pamorosogpd, scalar = pAmorosoGpd, q = c(0.8, 1.4, 3.0),
         args = list(loc = 0.0, scale = 1.0, shape1 = 2.0, shape2 = 1.1,
                     threshold = 1.6, tail_scale = 0.8, tail_shape = 0.15)),
    list(vec = pamoroso, scalar = pAmoroso, q = c(1.2, 1.5, 2.0),
         args = list(loc = 0.0, scale = 1.2, shape1 = 2.0, shape2 = 1.5)),
    list(vec = pcauchy_vec, scalar = pCauchy, q = c(-1.0, 0.0, 1.0),
         args = list(location = 0.0, scale = 1.1)),
    list(vec = pcauchymix, scalar = pCauchyMix, q = c(-1.0, 0.5, 1.2),
         args = list(w = c(0.7, 0.3), location = c(-0.5, 0.8), scale = c(1.0, 1.5)))
  )

  for (spec in p_specs) {
    vec <- do.call(spec$vec, c(list(spec$q), spec$args, list(lower.tail = FALSE, log.p = TRUE)))
    scalar <- vapply(
      spec$q,
      function(xx) as.numeric(do.call(spec$scalar, c(list(xx), spec$args, list(lower.tail = 0L, log.p = 1L)))),
      numeric(1)
    )
    expect_equal(vec, scalar, tolerance = 1e-10)
  }

  q_specs <- list(
    list(vec = qgpd, scalar = qGpd, p = c(0.1, 0.5, 0.9),
         args = list(threshold = 1.0, scale = 0.8, shape = 0.2)),
    list(vec = qnormmix, scalar = qNormMix, p = c(0.2, 0.5, 0.8),
         args = list(w = c(0.7, 0.3), mean = c(0.0, 1.2), sd = c(1.0, 1.6))),
    list(vec = qnormmixgpd, scalar = qNormMixGpd, p = c(0.2, 0.5, 0.8),
         args = list(w = c(0.6, 0.4), mean = c(0.0, 1.5), sd = c(1.0, 1.8),
                     threshold = 1.2, tail_scale = 1.0, tail_shape = 0.15)),
    list(vec = qnormgpd, scalar = qNormGpd, p = c(0.2, 0.5, 0.8),
         args = list(mean = 0.0, sd = 1.0, threshold = 1.2, tail_scale = 1.0, tail_shape = 0.15)),
    list(vec = qgammamix, scalar = qGammaMix, p = c(0.2, 0.5, 0.8),
         args = list(w = c(0.6, 0.4), shape = c(2.0, 4.0), scale = c(1.0, 2.0))),
    list(vec = qgammamixgpd, scalar = qGammaMixGpd, p = c(0.2, 0.5, 0.8),
         args = list(w = c(0.6, 0.4), shape = c(2.0, 4.0), scale = c(1.0, 2.0),
                     threshold = 2.5, tail_scale = 0.9, tail_shape = 0.2)),
    list(vec = qgammagpd, scalar = qGammaGpd, p = c(0.2, 0.5, 0.8),
         args = list(shape = 2.0, scale = 1.0, threshold = 2.5, tail_scale = 0.9, tail_shape = 0.2)),
    list(vec = qlognormalmix, scalar = qLognormalMix, p = c(0.2, 0.5, 0.8),
         args = list(w = c(0.7, 0.3), meanlog = c(0.0, 0.4), sdlog = c(0.3, 0.6))),
    list(vec = qlognormalmixgpd, scalar = qLognormalMixGpd, p = c(0.2, 0.5, 0.8),
         args = list(w = c(0.7, 0.3), meanlog = c(0.0, 0.4), sdlog = c(0.3, 0.6),
                     threshold = 1.8, tail_scale = 0.8, tail_shape = 0.1)),
    list(vec = qlognormalgpd, scalar = qLognormalGpd, p = c(0.2, 0.5, 0.8),
         args = list(meanlog = 0.0, sdlog = 0.3, threshold = 1.8, tail_scale = 0.8, tail_shape = 0.1)),
    list(vec = qlaplacemix, scalar = qLaplaceMix, p = c(0.2, 0.5, 0.8),
         args = list(w = c(0.7, 0.3), location = c(-0.5, 1.0), scale = c(1.0, 1.4))),
    list(vec = qlaplacemixgpd, scalar = qLaplaceMixGpd, p = c(0.2, 0.5, 0.8),
         args = list(w = c(0.7, 0.3), location = c(-0.5, 1.0), scale = c(1.0, 1.4),
                     threshold = 0.8, tail_scale = 0.9, tail_shape = 0.15)),
    list(vec = qlaplacegpd, scalar = qLaplaceGpd, p = c(0.2, 0.5, 0.8),
         args = list(location = -0.5, scale = 1.0, threshold = 0.8, tail_scale = 0.9, tail_shape = 0.15)),
    list(vec = qinvgaussmix, scalar = qInvGaussMix, p = c(0.2, 0.5, 0.8),
         args = list(w = c(0.6, 0.4), mean = c(1.2, 2.0), shape = c(2.5, 4.0))),
    list(vec = qinvgaussmixgpd, scalar = qInvGaussMixGpd, p = c(0.2, 0.5, 0.8),
         args = list(w = c(0.6, 0.4), mean = c(1.2, 2.0), shape = c(2.5, 4.0),
                     threshold = 1.6, tail_scale = 0.7, tail_shape = 0.15)),
    list(vec = qinvgaussgpd, scalar = qInvGaussGpd, p = c(0.2, 0.5, 0.8),
         args = list(mean = 1.2, shape = 2.5, threshold = 1.6, tail_scale = 0.7, tail_shape = 0.15)),
    list(vec = qinvgauss, scalar = qInvGauss, p = c(0.2, 0.5, 0.8),
         args = list(mean = 2.0, shape = 3.0)),
    list(vec = qamorosomix, scalar = qAmorosoMix, p = c(0.2, 0.5, 0.8),
         args = list(w = c(0.5, 0.3, 0.2), loc = c(0.0, 0.5, 1.0), scale = c(1.0, 1.2, 1.5),
                     shape1 = c(2.0, 3.0, 4.0), shape2 = c(1.1, 1.2, 1.3))),
    list(vec = qamorosomixgpd, scalar = qAmorosoMixGpd, p = c(0.2, 0.5, 0.8),
         args = list(w = c(0.5, 0.3, 0.2), loc = c(0.0, 0.5, 1.0), scale = c(1.0, 1.2, 1.5),
                     shape1 = c(2.0, 3.0, 4.0), shape2 = c(1.1, 1.2, 1.3),
                     threshold = 1.6, tail_scale = 0.8, tail_shape = 0.15)),
    list(vec = qamorosogpd, scalar = qAmorosoGpd, p = c(0.2, 0.5, 0.8),
         args = list(loc = 0.0, scale = 1.0, shape1 = 2.0, shape2 = 1.1,
                     threshold = 1.6, tail_scale = 0.8, tail_shape = 0.15)),
    list(vec = qamoroso, scalar = qAmoroso, p = c(0.2, 0.5, 0.8),
         args = list(loc = 0.0, scale = 1.2, shape1 = 2.0, shape2 = 1.5)),
    list(vec = qcauchy_vec, scalar = qCauchy, p = c(0.2, 0.5, 0.8),
         args = list(location = 0.0, scale = 1.1)),
    list(vec = qcauchymix, scalar = qCauchyMix, p = c(0.2, 0.5, 0.8),
         args = list(w = c(0.7, 0.3), location = c(-0.5, 0.8), scale = c(1.0, 1.5)))
  )

  for (spec in q_specs) {
    vec <- do.call(spec$vec, c(list(log(spec$p)), spec$args, list(lower.tail = TRUE, log.p = TRUE)))
    scalar <- vapply(
      log(spec$p),
      function(pp) do.call(spec$scalar, c(list(pp), spec$args, list(lower.tail = TRUE, log.p = TRUE))),
      numeric(1)
    )
    expect_equal(vec, scalar, tolerance = 1e-10)
  }
})

test_that("vectorized r functions match scalar draws with same seed", {
  r_specs <- list(
    list(vec = rgpd, scalar = rGpd, args = list(threshold = 1.0, scale = 0.8, shape = 0.2)),
    list(vec = rnormmix, scalar = rNormMix, args = list(w = c(0.7, 0.3), mean = c(0.0, 1.2), sd = c(1.0, 1.6))),
    list(vec = rnormmixgpd, scalar = rNormMixGpd, args = list(w = c(0.6, 0.4), mean = c(0.0, 1.5), sd = c(1.0, 1.8),
                                                               threshold = 1.2, tail_scale = 1.0, tail_shape = 0.15)),
    list(vec = rnormgpd, scalar = rNormGpd, args = list(mean = 0.0, sd = 1.0, threshold = 1.2, tail_scale = 1.0, tail_shape = 0.15)),
    list(vec = rgammamix, scalar = rGammaMix, args = list(w = c(0.6, 0.4), shape = c(2.0, 4.0), scale = c(1.0, 2.0))),
    list(vec = rgammamixgpd, scalar = rGammaMixGpd, args = list(w = c(0.6, 0.4), shape = c(2.0, 4.0), scale = c(1.0, 2.0),
                                                                 threshold = 2.5, tail_scale = 0.9, tail_shape = 0.2)),
    list(vec = rgammagpd, scalar = rGammaGpd, args = list(shape = 2.0, scale = 1.0, threshold = 2.5, tail_scale = 0.9, tail_shape = 0.2)),
    list(vec = rlognormalmix, scalar = rLognormalMix, args = list(w = c(0.7, 0.3), meanlog = c(0.0, 0.4), sdlog = c(0.3, 0.6))),
    list(vec = rlognormalmixgpd, scalar = rLognormalMixGpd, args = list(w = c(0.7, 0.3), meanlog = c(0.0, 0.4), sdlog = c(0.3, 0.6),
                                                                       threshold = 1.8, tail_scale = 0.8, tail_shape = 0.1)),
    list(vec = rlognormalgpd, scalar = rLognormalGpd, args = list(meanlog = 0.0, sdlog = 0.3, threshold = 1.8, tail_scale = 0.8, tail_shape = 0.1)),
    list(vec = rlaplacemix, scalar = rLaplaceMix, args = list(w = c(0.7, 0.3), location = c(-0.5, 1.0), scale = c(1.0, 1.4))),
    list(vec = rlaplacemixgpd, scalar = rLaplaceMixGpd, args = list(w = c(0.7, 0.3), location = c(-0.5, 1.0), scale = c(1.0, 1.4),
                                                                     threshold = 0.8, tail_scale = 0.9, tail_shape = 0.15)),
    list(vec = rlaplacegpd, scalar = rLaplaceGpd, args = list(location = -0.5, scale = 1.0, threshold = 0.8, tail_scale = 0.9, tail_shape = 0.15)),
    list(vec = rinvgaussmix, scalar = rInvGaussMix, args = list(w = c(0.6, 0.4), mean = c(1.2, 2.0), shape = c(2.5, 4.0))),
    list(vec = rinvgaussmixgpd, scalar = rInvGaussMixGpd, args = list(w = c(0.6, 0.4), mean = c(1.2, 2.0), shape = c(2.5, 4.0),
                                                                     threshold = 1.6, tail_scale = 0.7, tail_shape = 0.15)),
    list(vec = rinvgaussgpd, scalar = rInvGaussGpd, args = list(mean = 1.2, shape = 2.5, threshold = 1.6, tail_scale = 0.7, tail_shape = 0.15)),
    list(vec = rinvgauss, scalar = rInvGauss, args = list(mean = 2.0, shape = 3.0)),
    list(vec = ramorosomix, scalar = rAmorosoMix, args = list(w = c(0.5, 0.3, 0.2), loc = c(0.0, 0.5, 1.0), scale = c(1.0, 1.2, 1.5),
                                                             shape1 = c(2.0, 3.0, 4.0), shape2 = c(1.1, 1.2, 1.3))),
    list(vec = ramorosomixgpd, scalar = rAmorosoMixGpd, args = list(w = c(0.5, 0.3, 0.2), loc = c(0.0, 0.5, 1.0), scale = c(1.0, 1.2, 1.5),
                                                                   shape1 = c(2.0, 3.0, 4.0), shape2 = c(1.1, 1.2, 1.3),
                                                                   threshold = 1.6, tail_scale = 0.8, tail_shape = 0.15)),
    list(vec = ramorosogpd, scalar = rAmorosoGpd, args = list(loc = 0.0, scale = 1.0, shape1 = 2.0, shape2 = 1.1,
                                                             threshold = 1.6, tail_scale = 0.8, tail_shape = 0.15)),
    list(vec = ramoroso, scalar = rAmoroso, args = list(loc = 0.0, scale = 1.2, shape1 = 2.0, shape2 = 1.5)),
    list(vec = rcauchy_vec, scalar = rCauchy, args = list(location = 0.0, scale = 1.1)),
    list(vec = rcauchymix, scalar = rCauchyMix, args = list(w = c(0.7, 0.3), location = c(-0.5, 0.8), scale = c(1.0, 1.5)))
  )

  for (spec in r_specs) {
    set.seed(123)
    vec <- do.call(spec$vec, c(list(n = 7), spec$args))
    set.seed(123)
    scalar <- vapply(
      seq_len(7),
      function(i) do.call(spec$scalar, c(list(n = 1), spec$args)),
      numeric(1)
    )
    expect_equal(vec, scalar, tolerance = 1e-10)
  }
})

# ===== END unit/test-vectorization.R =====

# ===== BEGIN unit/test-vectorization-contract.R =====
test_that("lowercase r* supports n = 0", {

  w <- c(0.60, 0.40)
  shape <- c(2.0, 5.0)
  scale <- c(1.0, 2.0)

  # Use lowercase vectorized wrapper
  draws <- rgammamix(0, w = w, shape = shape, scale = scale)
  expect_true(is.numeric(draws))
  expect_length(draws, 0)
})

# ===== END unit/test-vectorization-contract.R =====

# ===== BEGIN unit/test-visualization-helpers.R =====
# tests/testthat/test-visualization-helpers.R
# Unit tests for internal visualization helper functions (06-visualization-helpers.R)

# ======================================================================
# .plot_palette tests
# ======================================================================

test_that(".plot_palette returns correct number of colors", {
  expect_length(.plot_palette(3), 3)
  expect_length(.plot_palette(8), 8)
  expect_length(.plot_palette(1), 1)
})

test_that(".plot_palette recycles when n > 8", {
  pal <- .plot_palette(12)
  expect_length(pal, 12)
  # First 8 should repeat in positions 9-12
  expect_equal(pal[9], pal[1])
  expect_equal(pal[10], pal[2])
})

test_that(".plot_palette returns valid hex colors", {
  pal <- .plot_palette(8)
  expect_true(all(grepl("^#[0-9A-Fa-f]{6}$", pal)))
})

test_that(".plot_palette handles NULL input", {
  pal <- .plot_palette(NULL)
  expect_length(pal, 8)  # default length
})

# ======================================================================
# .plot_theme tests
# ======================================================================

test_that(".plot_theme returns a ggplot theme", {
  skip_if_not_installed("ggplot2")
  theme <- .plot_theme()
  expect_s3_class(theme, "theme")
})

# ======================================================================
# .plot_quantile_pred tests
# ======================================================================

test_that(".plot_quantile_pred works with data.frame fit", {
  skip_if_not_installed("ggplot2")
  pred <- list(
    fit = data.frame(
      index = c(0.25, 0.5, 0.75),
      estimate = c(1, 2, 3),
      lower = c(0.5, 1.5, 2.5),
      upper = c(1.5, 2.5, 3.5)
    )
  )
  p <- .plot_quantile_pred(pred)
  expect_s3_class(p, "gg")
})

test_that(".plot_quantile_pred works with id column", {
  skip_if_not_installed("ggplot2")
  pred <- list(
    fit = data.frame(
      id = rep(1:2, each = 3),
      index = rep(c(0.25, 0.5, 0.75), 2),
      estimate = rnorm(6),
      lower = rnorm(6) - 0.5,
      upper = rnorm(6) + 0.5
    )
  )
  p <- .plot_quantile_pred(pred)
  expect_s3_class(p, "gg")
})

test_that(".plot_quantile_pred errors on non-data.frame fit", {
  pred <- list(fit = c(1, 2, 3))
  expect_error(.plot_quantile_pred(pred), "data frame")
})

# ======================================================================
# .plot_sample_pred tests
# ======================================================================

test_that(".plot_sample_pred works with numeric samples", {
  skip_if_not_installed("ggplot2")
  pred <- list(fit = rnorm(100))
  p <- .plot_sample_pred(pred)
  expect_s3_class(p, "gg")
})

test_that(".plot_sample_pred errors on non-numeric", {
  pred <- list(fit = data.frame(x = 1:5))
  expect_error(.plot_sample_pred(pred), "numeric vector")
})

# ======================================================================
# .plot_mean_pred tests
# ======================================================================

test_that(".plot_mean_pred works with data.frame fit and draws", {
  skip_if_not_installed("ggplot2")
  pred <- list(
    fit = data.frame(estimate = 5.0, lower = 4.5, upper = 5.5),
    draws = rnorm(100, mean = 5)
  )
  p <- .plot_mean_pred(pred)
  expect_s3_class(p, "gg")
})

test_that(".plot_mean_pred works with data.frame fit without draws", {
  skip_if_not_installed("ggplot2")
  pred <- list(
    fit = data.frame(estimate = 5.0, lower = 4.5, upper = 5.5),
    draws = NULL
  )
  p <- .plot_mean_pred(pred)
  expect_s3_class(p, "gg")
})

test_that(".plot_mean_pred works with numeric fit (old format)", {
  skip_if_not_installed("ggplot2")
  pred <- list(
    fit = c(4.5, 5.0, 5.5),
    draws = rnorm(100, mean = 5)
  )
  p <- .plot_mean_pred(pred)
  expect_s3_class(p, "gg")
})

test_that(".plot_mean_pred works without CI bounds", {
  skip_if_not_installed("ggplot2")
  pred <- list(
    fit = data.frame(estimate = 5.0),
    draws = rnorm(100, mean = 5)
  )
  p <- .plot_mean_pred(pred)
  expect_s3_class(p, "gg")
})

# ======================================================================
# .plot_density_pred tests
# ======================================================================

test_that(".plot_density_pred works with data.frame fit", {
  skip_if_not_installed("ggplot2")
  y_grid <- seq(0, 5, length.out = 20)
  pred <- list(
    fit = data.frame(
      y = y_grid,
      density = dnorm(y_grid, mean = 2.5)
    )
  )
  p <- .plot_density_pred(pred)
  expect_s3_class(p, "gg")
})

test_that(".plot_density_pred works with id column", {
  skip_if_not_installed("ggplot2")
  y_grid <- seq(0, 5, length.out = 10)
  pred <- list(
    fit = data.frame(
      id = rep(1:2, each = 10),
      y = rep(y_grid, 2),
      density = c(dnorm(y_grid, 2), dnorm(y_grid, 3))
    )
  )
  p <- .plot_density_pred(pred)
  expect_s3_class(p, "gg")
})

test_that(".plot_density_pred works with numeric vector fit", {
  skip_if_not_installed("ggplot2")
  y_grid <- seq(0, 5, length.out = 20)
  pred <- list(
    fit = dnorm(y_grid, mean = 2.5),
    grid = y_grid
  )
  p <- .plot_density_pred(pred)
  expect_s3_class(p, "gg")
})

test_that(".plot_density_pred handles grid column name", {
  skip_if_not_installed("ggplot2")
  pred <- list(
    fit = data.frame(
      grid = seq(0, 5, length.out = 10),
      density = dnorm(seq(0, 5, length.out = 10), mean = 2.5)
    )
  )
  p <- .plot_density_pred(pred)
  expect_s3_class(p, "gg")
})

# ======================================================================
# .plot_survival_pred tests
# ======================================================================

test_that(".plot_survival_pred works with data.frame fit", {
  skip_if_not_installed("ggplot2")
  y_grid <- seq(0, 5, length.out = 20)
  pred <- list(
    fit = data.frame(
      y = y_grid,
      survival = 1 - pnorm(y_grid, mean = 2.5)
    )
  )
  p <- .plot_survival_pred(pred)
  expect_s3_class(p, "gg")
})

test_that(".plot_survival_pred works with id column", {
  skip_if_not_installed("ggplot2")
  y_grid <- seq(0, 5, length.out = 10)
  pred <- list(
    fit = data.frame(
      id = rep(1:2, each = 10),
      y = rep(y_grid, 2),
      survival = c(1 - pnorm(y_grid, 2), 1 - pnorm(y_grid, 3))
    )
  )
  p <- .plot_survival_pred(pred)
  expect_s3_class(p, "gg")
})

test_that(".plot_survival_pred works with numeric vector fit", {
  skip_if_not_installed("ggplot2")
  y_grid <- seq(0, 5, length.out = 20)
  pred <- list(
    fit = 1 - pnorm(y_grid, mean = 2.5),
    grid = y_grid
  )
  p <- .plot_survival_pred(pred)
  expect_s3_class(p, "gg")
})

# ======================================================================
# .plot_location_pred tests
# ======================================================================

test_that(".plot_location_pred works with single row (no id)", {
  skip_if_not_installed("ggplot2")
  pred <- list(
    fit = data.frame(
      mean = 5.0, mean_lower = 4.5, mean_upper = 5.5,
      median = 4.9, median_lower = 4.4, median_upper = 5.4
    )
  )
  p <- .plot_location_pred(pred)
  expect_s3_class(p, "gg")
})

test_that(".plot_location_pred works with id column", {
  skip_if_not_installed("ggplot2")
  pred <- list(
    fit = data.frame(
      id = 1:3,
      mean = c(5.0, 5.5, 6.0),
      mean_lower = c(4.5, 5.0, 5.5),
      mean_upper = c(5.5, 6.0, 6.5),
      median = c(4.9, 5.4, 5.9),
      median_lower = c(4.4, 4.9, 5.4),
      median_upper = c(5.4, 5.9, 6.4)
    )
  )
  p <- .plot_location_pred(pred)
  expect_s3_class(p, "gg")
})

test_that(".plot_location_pred errors on non-data.frame fit", {
  pred <- list(fit = c(1, 2, 3))
  expect_error(.plot_location_pred(pred), "data frame")
})

# ===== END unit/test-visualization-helpers.R =====

# ===== BEGIN unit/test-formatting-and-wrapper-coverage.R =====

test_that("formatting helpers handle numeric, matrix, and data.frame inputs", {
  expect_equal(fmt3(c(1, 1.2345)), c("1", "1.234"))
  expect_true(grepl("e", fmt3_sci(c(1, 100000), big = 1000)[2], fixed = TRUE))
  expect_equal(fmt3_vec(numeric(0)), "")
  expect_match(fmt3_vec(c(1, 2.5)), "1, 2.5")

  df <- data.frame(a = c(1.2345, 2.3456), b = c("x", "y"), stringsAsFactors = FALSE)
  df_fmt <- format_df3(df)
  expect_equal(df_fmt$a, c("1.234", "2.346"))
  expect_equal(df_fmt$b, c("x", "y"))

  df_sci <- format_df3_sci(data.frame(a = c(1, 1e5)))
  expect_true(grepl("e", df_sci$a[2], fixed = TRUE))

  mat <- matrix(c(1.2345, 200000, 3.4567, 4.5678), nrow = 2, byrow = TRUE)
  colnames(mat) <- c("x", "y")
  mat_fmt <- format_mat3(mat)
  mat_sci <- format_mat3_sci(mat, big = 1000)
  expect_equal(dim(mat_fmt), dim(mat))
  expect_equal(dimnames(mat_fmt), dimnames(mat))
  expect_true(grepl("e", mat_sci[1, 2], fixed = TRUE))
})

test_that("knitr formatting helpers return stable defaults outside knitr", {
  old_opt <- getOption("knitr.in.progress")
  options(knitr.in.progress = FALSE)
  on.exit(options(knitr.in.progress = old_opt), add = TRUE)

  expect_false(.is_knitr_output())
  expect_equal(.kable_fmt(), "markdown")
})

test_that("wrapper helpers normalize treatment and formula inputs", {
  expect_equal(.coerce_treat(factor(c("control", "treated", "control"))), c(0L, 1L, 0L))
  expect_equal(.coerce_treat(c(FALSE, TRUE, TRUE)), c(0L, 1L, 1L))
  expect_equal(.coerce_treat(c("a", "b", "a")), c(0L, 1L, 0L))
  expect_error(.coerce_treat(c(0, 1, NA)), "cannot contain NA")
  expect_true(.treat_arg_supplied(list(treat = quote(trt)), quote(trt)))
  expect_false(.treat_arg_supplied(list(), quote(NULL)))
  expect_false(.treat_arg_supplied(list(treat = quote(NULL)), quote(NULL)))

  dat <- data.frame(
    y = c(1, 2, 3),
    x1 = c(0.1, 0.2, 0.3),
    id = c("a", "b", "c"),
    trt = c(0, 1, 0)
  )
  parsed <- .parse_formula_yX(y ~ x1 + id, data = dat)
  expect_equal(parsed$y, c(1, 2, 3))
  expect_true(is.matrix(parsed$X))
  expect_equal(colnames(parsed$X), "x1")
  expect_false(parsed$is_unconditional)

  mf <- stats::model.frame(y ~ x1, data = dat)
  expect_equal(.extract_treat_from_data(mf, dat, "trt"), c(0L, 1L, 0L))
  expect_error(.extract_treat_from_data(mf, dat, c(0, 1)), "length does not match")
})

test_that("mcmc override helpers split runner controls and update bundles", {
  parsed <- .normalize_mcmc_inputs(
    list(
      niter = 50,
      nburn = 10,
      show_progress = FALSE,
      timing = TRUE,
      mcmc_outcome = list(seed = 9),
      mcmc_ps = list(niter = 20)
    )
  )
  expect_equal(parsed$overrides$niter, 50)
  expect_equal(parsed$overrides$nburnin, 10)
  expect_false("nburn" %in% names(parsed$overrides))
  expect_false(parsed$runner$show_progress)
  expect_true(parsed$runner$timing)
  expect_equal(parsed$outcome_overrides$seed, 9)
  expect_equal(parsed$ps_overrides$niter, 20)
  expect_error(.normalize_mcmc_inputs(list(10)), "must be named")
  expect_error(.normalize_mcmc_inputs(list(foo = 1)), "Unknown mcmc argument")
  expect_error(.normalize_mcmc_inputs(list(mcmc_ps = 1)), "must be a named list")

  bundle_obj <- structure(list(mcmc = list(niter = 20, nburnin = 5)), class = "causalmixgpd_bundle")
  updated <- .apply_mcmc_overrides(bundle_obj, list(niter = 100, seed = 9))
  expect_equal(updated$mcmc$niter, 100)
  expect_equal(updated$mcmc$seed, 9)

  causal_bundle_obj <- structure(
    list(
      outcome = list(
        con = list(mcmc = list(niter = 20, nburnin = 5)),
        trt = list(mcmc = list(niter = 20, nburnin = 5))
      ),
      design = structure(list(mcmc = list(niter = 12, nburnin = 3)), class = "causalmixgpd_ps_bundle")
    ),
    class = "causalmixgpd_causal_bundle"
  )
  updated_causal <- .apply_mcmc_overrides(
    causal_bundle_obj,
    list(niter = 100),
    outcome_overrides = list(seed = 7),
    ps_overrides = list(niter = 30, seed = 11)
  )
  expect_equal(updated_causal$outcome$con$mcmc$niter, 100)
  expect_equal(updated_causal$outcome$trt$mcmc$seed, 7)
  expect_equal(updated_causal$design$mcmc$niter, 30)
  expect_equal(updated_causal$design$mcmc$seed, 11)
  expect_error(.apply_mcmc_overrides(bundle_obj, list(niter = 10), ps_overrides = list(niter = 5)), "only for causal bundles")
})

test_that("build-run helpers validate inputs and expose structural metadata", {
  y <- c(1, 2, 3)
  X <- data.frame(x1 = c(0.1, 0.2, 0.3), x2 = c(0.4, 0.5, 0.6))
  built_data <- build_data_from_inputs(y = y, X = X, ps = c(0.2, 0.4, 0.6))
  expect_equal(names(built_data), c("y", "X", "ps"))
  expect_true(is.matrix(built_data$X))
  expect_error(build_data_from_inputs(y = y, X = X[1:2, ]), "nrow\\(X\\) == length\\(y\\)")

  spec <- compile_model_spec(
    y = abs(stats::rnorm(10)) + 0.1,
    X = as.matrix(X[rep(1:3, length.out = 10), ]),
    backend = "sb",
    kernel = "normal",
    GPD = TRUE,
    components = 4
  )
  dims <- build_dimensions_from_spec(spec)
  mons <- build_monitors_from_spec(spec, monitor_v = TRUE, monitor_latent = TRUE)
  expect_true(is.list(dims))
  expect_true(all(c("v", "w", "z") %in% names(dims)))
  expect_true(any(startsWith(names(dims), "beta_")))
  expect_true(any(grepl("^w\\[", mons)))
  expect_true(any(grepl("^v\\[", mons)))
})

test_that("compile_model_spec aligns backend default links and validates link-mode priors", {
  y <- abs(stats::rnorm(8)) + 0.1
  X <- cbind(
    x1 = seq(-1, 1, length.out = 8),
    x2 = rep(c(-0.5, 0.5), length.out = 8)
  )

  specs <- lapply(c("sb", "crp", "spliced"), function(be) {
    compile_model_spec(
      y = y,
      X = X,
      backend = be,
      kernel = "normal",
      GPD = TRUE,
      components = 3L
    )
  })
  names(specs) <- c("sb", "crp", "spliced")

  expect_true(all(vapply(specs, function(sp) identical(sp$plan$bulk$mean$mode, "link"), logical(1))))
  expect_true(all(vapply(specs, function(sp) identical(sp$plan$bulk$sd$mode, "dist"), logical(1))))
  expect_true(all(vapply(specs, function(sp) identical(sp$plan$gpd$threshold$mode, "link"), logical(1))))
  expect_true(all(vapply(specs, function(sp) identical(sp$plan$gpd$tail_scale$mode, "link"), logical(1))))
  expect_true(all(vapply(specs, function(sp) identical(sp$plan$gpd$tail_shape$mode, "dist"), logical(1))))
  expect_true(all(vapply(specs, function(sp) identical(sp$plan$bulk$mean$beta_prior$dist, "normal"), logical(1))))
  expect_true(all(vapply(specs, function(sp) identical(sp$plan$gpd$tail_scale$beta_prior$dist, "normal"), logical(1))))
  expect_equal(specs$sb$plan$gpd$threshold$link_dist$dist, "lognormal")
  expect_equal(specs$crp$plan$gpd$threshold$link_dist$dist, "lognormal")
  expect_equal(specs$spliced$plan$gpd$threshold$link_dist$dist, "lognormal")

  expect_error(
    compile_model_spec(
      y = y,
      X = X,
      backend = "sb",
      kernel = "normal",
      GPD = FALSE,
      components = 3L,
      param_specs = list(
        bulk = list(
          sd = list(mode = "link", link = "identity")
        )
      )
    ),
    "positive-support parameter 'sd'"
  )

  expect_error(
    compile_model_spec(
      y = y,
      X = X,
      backend = "sb",
      kernel = "normal",
      GPD = TRUE,
      components = 3L,
      param_specs = list(
        gpd = list(
          tail_scale = list(mode = "link", link = "identity")
        )
      )
    ),
    "positive-support parameter 'tail_scale'"
  )

  expect_error(
    compile_model_spec(
      y = y,
      X = X,
      backend = "sb",
      kernel = "normal",
      GPD = FALSE,
      components = 3L,
      param_specs = list(
        bulk = list(
          mean = list(
            mode = "link",
            beta_prior = list(dist = "lognormal", args = list(meanlog = 0, sdlog = 0.5))
          )
        )
      )
    ),
    "must use dist = 'normal'"
  )

  spliced_link_dist <- compile_model_spec(
    y = y,
    X = X,
    backend = "spliced",
    kernel = "normal",
    GPD = TRUE,
    components = 3L,
    param_specs = list(
      gpd = list(
        threshold = list(mode = "link", link_dist = list(dist = "lognormal"))
      )
    )
  )
  expect_equal(spliced_link_dist$plan$gpd$threshold$link_dist$dist, "lognormal")
  expect_equal(spliced_link_dist$plan$gpd$sdlog_u$dist, "invgamma")

  spliced_link_dims <- build_dimensions_from_spec(spliced_link_dist)
  spliced_link_mons <- build_monitors_from_spec(spliced_link_dist)
  spliced_link_inits <- build_inits_from_spec(spliced_link_dist, y = y)

  expect_true(all(c("beta_threshold", "threshold_i") %in% names(spliced_link_dims)))
  expect_true(any(grepl("^beta_threshold\\[", spliced_link_mons)))
  expect_true(any(grepl("^threshold_i\\[", spliced_link_mons)))
  expect_true("sdlog_u" %in% spliced_link_mons)
  expect_true(all(c("beta_threshold", "threshold_i", "sdlog_u") %in% names(spliced_link_inits)))

  bundle_spliced <- build_nimble_bundle(
    y = y,
    X = X,
    backend = "spliced",
    kernel = "normal",
    GPD = TRUE,
    components = 3L,
    param_specs = list(
      gpd = list(
        threshold = list(mode = "link", link_dist = list(dist = "lognormal"))
      )
    )
  )
  code_spliced <- paste(deparse(.extract_nimble_code(bundle_spliced$code)), collapse = "\n")
  expect_true(any(grepl("^threshold_i\\[", bundle_spliced$monitors)))
  expect_match(code_spliced, "threshold_i\\[i, k\\] ~ dlnorm")
  expect_match(code_spliced, "threshold_i\\[i, z\\[i\\]\\]")
})

test_that(".configure_samplers preserves conjugate beta_threshold updates", {
  set.seed(1)
  y <- exp(stats::rnorm(12))
  X <- cbind(
    x1 = as.numeric(scale(stats::rnorm(12))),
    x2 = as.numeric(scale(stats::rnorm(12)))
  )

  for (be in c("sb", "crp")) {
    bundle <- build_nimble_bundle(
      y = y,
      X = X,
      backend = be,
      kernel = "lognormal",
      GPD = TRUE,
      components = 3L,
      mcmc = list(niter = 60, nburnin = 20, thin = 2, nchains = 1, seed = 1)
    )
    code <- .extract_nimble_code(bundle$code)
    model <- nimble::nimbleModel(
      code = code,
      data = bundle$data,
      constants = bundle$constants,
      inits = bundle$inits,
      dimensions = bundle$dimensions,
      check = FALSE,
      calculate = FALSE
    )
    conf <- nimble::configureMCMC(model, monitors = bundle$monitors, enableWAIC = TRUE)
    conf <- .configure_samplers(
      conf,
      spec = bundle$spec,
      data_info = list(constants = bundle$constants, dimensions = bundle$dimensions),
      z_update_every = 1L
    )

    has_threshold_sampler <- any(vapply(conf$getSamplers(), function(sc) {
      tgt <- sc$target %||% character(0)
      any(grepl("^beta_threshold\\[", tgt))
    }, logical(1)))

    expect_true(has_threshold_sampler, info = be)
  }
})

test_that("check_glue_validity covers validation and continuity branches", {
  skip_if_not_test_level("ci")

  fit <- .load_fixture("fit_gpd_small.rds")
  expect_error(
    check_glue_validity(fit, grid = c(0.1, NA_real_)),
    "'grid' must be finite numeric"
  )
  expect_error(
    check_glue_validity(fit, n_draws = 0L),
    "'n_draws' must be >= 1"
  )

  out <- check_glue_validity(
    fit,
    grid = seq(0.1, 4, length.out = 25),
    n_draws = 3L,
    check_continuity = TRUE
  )
  expect_true(is.list(out$violations))
  expect_true("continuity" %in% names(out$pass))

  expect_error(
    check_glue_validity(fit, x = matrix(1, ncol = 1)),
    "Unconditional model: 'x' not allowed"
  )
})

# ===== END unit/test-formatting-and-wrapper-coverage.R =====

# ===== BEGIN unit/test-causal-and-cluster-helper-coverage.R =====

predict.fake_mixfit <- function(object,
                                newdata = NULL,
                                x = NULL,
                                y = NULL,
                                ps = NULL,
                                id = NULL,
                                type = c("mean", "quantile", "density", "survival", "prob", "fit", "rmean", "location"),
                                index = NULL,
                                interval = NULL,
                                probs = c(0.025, 0.5, 0.975),
                                store_draws = FALSE,
                                nsim_mean = 200L,
                                ...) {
  type <- match.arg(type)
  arm_shift <- if (identical(object$arm, "trt")) 1 else 0
  x_mat <- if (!is.null(newdata)) as.matrix(newdata) else if (!is.null(x)) as.matrix(x) else NULL
  n_pred <- if (!is.null(x_mat)) nrow(x_mat) else if (!is.null(y)) length(y) else 1L
  id_vals <- if (!is.null(id)) id else seq_len(n_pred)

  if (type %in% c("density", "survival")) {
    est <- arm_shift + ifelse(type == "density", 0.2, 0.8) + seq_len(n_pred) / 100
    return(list(
      fit = data.frame(
        id = id_vals,
        estimate = est,
        lower = est - 0.05,
        upper = est + 0.05
      )
    ))
  }

  if (type %in% c("mean", "rmean")) {
    est <- arm_shift + seq_len(n_pred)
    out <- list(
      fit = data.frame(
        id = id_vals,
        estimate = est,
        lower = est - 0.25,
        upper = est + 0.25
      ),
      diagnostics = if (type == "mean") list(mean_method = "analytic") else list()
    )
    if (isTRUE(store_draws)) {
      out$draws <- cbind(est - 0.2, est + 0.2)
      out$draws <- t(out$draws)
    }
    return(out)
  }

  if (type == "fit") {
    vals <- matrix(
      rep(arm_shift + seq_len(n_pred), each = 4L) + rep(c(-0.2, -0.05, 0.05, 0.2), times = n_pred),
      nrow = 4L
    )
    return(list(
      fit = data.frame(id = id_vals, estimate = colMeans(vals)),
      draws = vals
    ))
  }

  if (type == "quantile") {
    p <- as.numeric(index %||% 0.5)
    m <- length(p)
    if (isTRUE(object$no_quantile_draws)) {
      fit <- data.frame(
        id = rep(id_vals, each = m),
        index = rep(p, times = n_pred),
        estimate = rep(arm_shift + seq_len(n_pred), each = m) + rep(p, times = n_pred),
        lower = rep(arm_shift + seq_len(n_pred), each = m) + rep(p, times = n_pred) - 0.1,
        upper = rep(arm_shift + seq_len(n_pred), each = m) + rep(p, times = n_pred) + 0.1
      )
      return(list(fit = fit))
    }

    draws <- array(NA_real_, dim = c(3L, n_pred, m))
    for (j in seq_len(m)) {
      base <- arm_shift + seq_len(n_pred) + p[j]
      draws[, , j] <- rbind(base - 0.1, base, base + 0.1)
    }
    est <- apply(draws, c(2, 3), mean)
    lower <- apply(draws, c(2, 3), min)
    upper <- apply(draws, c(2, 3), max)
    fit <- data.frame(
      id = rep(id_vals, each = m),
      index = rep(p, times = n_pred),
      estimate = as.vector(t(est)),
      lower = as.vector(t(lower)),
      upper = as.vector(t(upper))
    )
    out <- list(fit = fit)
    if (isTRUE(store_draws)) out$draws <- draws
    out
  } else {
    stop("Unsupported fake predict type.", call. = FALSE)
  }
}

.make_fake_causal_fit <- function(has_X = TRUE, ps_enabled = FALSE, ps_fit = FALSE, no_quantile_draws = FALSE) {
  X <- if (isTRUE(has_X)) cbind(x1 = c(-1, 0, 1), x2 = c(0.5, 1.5, -0.5)) else NULL
  bundle <- list(
    data = list(X = X),
    index = list(trt = c(2L, 3L)),
    meta = list(
      has_x = isTRUE(has_X),
      ps = list(enabled = ps_enabled, include_in_outcome = ps_enabled, model_type = if (ps_enabled) "logit" else FALSE),
      ps_scale = "prob",
      ps_summary = "mean",
      ps_clamp = 1e-6,
      backend = list(trt = "sb", con = "sb"),
      kernel = list(trt = "normal", con = "normal"),
      GPD = list(trt = FALSE, con = FALSE)
    )
  )

  design <- NULL
  ps_fit_obj <- NULL
  if (isTRUE(ps_enabled) && isTRUE(has_X)) {
    design <- .build_ps_bundle(
      A = c(0L, 1L, 1L),
      X = X,
      spec = list(model = "logit", prior = list(mean = 0, sd = 1), include_intercept = TRUE),
      mcmc = mcmc_fast(seed = 11L)
    )
    bundle$design <- design
    if (isTRUE(ps_fit)) {
      ps_fit_obj <- list(mcmc = list(samples = matrix(
        c(0.1, 0.2, -0.1, -0.2, 0.3, 0.1),
        nrow = 2,
        byrow = TRUE,
        dimnames = list(NULL, c("beta[1]", "beta[2]", "beta[3]"))
      )))
    }
  }

  fit <- list(
    bundle = bundle,
    ps_fit = ps_fit_obj,
    ps_hat = if (isTRUE(ps_enabled) && !isTRUE(ps_fit) && isTRUE(has_X)) c(0.3, 0.6, 0.8) else NULL,
    outcome_fit = list(
      trt = structure(list(arm = "trt", no_quantile_draws = no_quantile_draws), class = "fake_mixfit"),
      con = structure(list(arm = "con", no_quantile_draws = no_quantile_draws), class = "fake_mixfit")
    )
  )
  class(fit) <- "causalmixgpd_causal_fit"
  fit
}

test_that("cluster formula parsing and override helpers cover validation branches", {
  dat <- data.frame(
    y = c(1, 2, 3),
    x1 = c(0.1, 0.2, 0.3),
    g = factor(c("a", "b", "a")),
    id = c("u1", "u2", "u3")
  )

  parsed <- .cluster_parse_formula(y ~ x1 + g + id, data = dat)
  expect_equal(parsed$y, c(1, 2, 3))
  expect_true(parsed$has_X)
  expect_true(all(c("x1", "gb") %in% colnames(parsed$X)))
  expect_false(any(grepl("^id", colnames(parsed$X))))
  expect_error(.cluster_parse_formula(NULL, dat), "'formula' is required")
  expect_error(.cluster_parse_formula(y ~ x1, NULL), "'data' is required")

  expect_equal(.cluster_validate_type(NULL, "param"), "param")
  expect_error(
    .cluster_validate_type_requirements("weights", has_X = FALSE, components_missing = FALSE),
    "requires covariates"
  )
  expect_error(
    .cluster_validate_type_requirements("both", has_X = TRUE, components_missing = TRUE),
    "explicit 'components'"
  )

  parsed_overrides <- .cluster_split_overrides(
    list(mean = "identity", tail_scale = "exp", concentration = 2),
    bulk_names = c("mean", "sd"),
    gpd_names = c("threshold", "tail_scale", "tail_shape")
  )
  expect_equal(parsed_overrides$bulk$mean, "identity")
  expect_equal(parsed_overrides$gpd$tail_scale, "exp")
  expect_equal(parsed_overrides$concentration, 2)
  parsed_overrides_mixed <- .cluster_split_overrides(
    list(
      bulk = list(mean = "identity"),
      tail_scale = "exp",
      alpha = 3
    ),
    bulk_names = c("mean", "sd"),
    gpd_names = c("threshold", "tail_scale", "tail_shape")
  )
  expect_equal(parsed_overrides_mixed$bulk$mean, "identity")
  expect_equal(parsed_overrides_mixed$gpd$tail_scale, "exp")
  expect_equal(parsed_overrides_mixed$concentration, 3)
  expect_error(
    .cluster_split_overrides(list(bad = 1), bulk_names = "mean", gpd_names = "tail_scale"),
    "Unknown override names"
  )

  expect_equal(.cluster_normalize_link_entry("softplus")$link, "softplus")
  expect_error(.cluster_normalize_link_entry(1), "strings or lists")

  spec <- list(
    plan = list(
      bulk = list(mean = list(mode = "link", link = "identity")),
      gpd = list(tail_scale = list(mode = "dist")),
      concentration = list(mode = "dist", dist = "gamma")
    )
  )
  spec2 <- .cluster_apply_link_overrides(
    spec,
    link = list(
      mean = "softplus",
      tail_scale = list(mode = "link", link = "exp", beta_prior = list(dist = "normal", args = list(mean = 0, sd = 1)))
    ),
    has_X = TRUE
  )
  expect_equal(spec2$plan$bulk$mean$link, "softplus")
  expect_equal(spec2$plan$gpd$tail_scale$link, "exp")
  expect_error(.cluster_apply_link_overrides(spec, link = list(mean = "identity"), has_X = FALSE), "require covariates")

  expect_equal(.cluster_normalize_prior_entry(2, current_mode = "dist")$mode, "fixed")
  expect_equal(.cluster_normalize_prior_entry("gamma", current_mode = "dist")$dist, "gamma")
  expect_equal(
    .cluster_normalize_prior_entry(list(dist = "normal", args = list(mean = 0, sd = 1)), current_mode = "link", param_name = "mean")$mode,
    "link"
  )
  expect_error(.cluster_normalize_prior_entry(TRUE, current_mode = "dist"), "numeric, character, or list")

  spec3 <- .cluster_apply_prior_overrides(
    spec2,
    priors = list(
      bulk = list(mean = list(value = 1.5)),
      gpd = list(tail_scale = list(dist = "normal", args = list(mean = 0, sd = 0.5))),
      concentration = list(value = 3)
    )
  )
  expect_equal(spec3$plan$bulk$mean$value, 1.5)
  expect_equal(spec3$plan$gpd$tail_scale$mode, "link")
  expect_equal(spec3$plan$gpd$tail_scale$beta_prior$dist, "normal")
  expect_equal(spec3$plan$gpd$tail_scale$beta_prior$args$sd, 0.5)
  expect_equal(spec3$plan$concentration$value, 3)
})

test_that("cluster posterior helper utilities cover draw and density logic", {
  skip_if_not_installed("coda")

  expect_equal(.cluster_draw_indices(5, burnin = 1, thin = 2), c(2L, 4L))
  expect_error(.cluster_draw_indices(5, burnin = 5), "too large")
  expect_error(.cluster_draw_indices(5, thin = 0), ">= 1")

  m <- coda::mcmc(matrix(
    c(1, 1, 2, 2, 2, 1, 1, 2),
    nrow = 2,
    byrow = TRUE,
    dimnames = list(NULL, c("z[1]", "z[2]", "z[3]", "z[4]"))
  ))
  ml <- coda::mcmc.list(m)
  draw_mat <- .cluster_samples_to_matrix(ml)
  z <- .cluster_extract_z_from_matrix(draw_mat)
  expect_equal(dim(z), c(2, 4))
  expect_error(.cluster_samples_to_matrix(matrix(1:4, nrow = 2)), "Expected 'mcmc' or 'mcmc.list'")
  expect_error(.cluster_extract_z_from_matrix(matrix(1:4, nrow = 2)), "No z\\[i\\] columns found")

  psm <- compute_psm(z)
  dahl <- dahl_labels(z, psm)
  scores <- .cluster_compute_scores(z, dahl$labels, psm)
  expect_equal(dim(psm), c(4, 4))
  expect_true(all(diag(psm) == 1))
  expect_equal(length(dahl$labels), 4)
  expect_equal(nrow(scores), 4)
  expect_equal(rowSums(scores), rep(1, 4), tolerance = 1e-8)

  expect_equal(.cluster_link_apply(2, "identity"), 2)
  expect_equal(.cluster_link_apply(0, "exp"), 1)
  expect_equal(.cluster_link_apply(exp(2), "log"), 2)
  expect_equal(.cluster_link_apply(2, "power", link_power = 3), 8)
  expect_error(.cluster_link_apply(2, "power", link_power = NA_real_), "Invalid power link exponent")
  expect_error(.cluster_link_apply(2, "weird"), "Unsupported link")

  expect_equal(.cluster_softmax(c(0, 0)), c(0.5, 0.5))
  gd <- .cluster_extract_gating_draw(
    c("eta[1]" = 0.5, "B[1,1]" = 1.0, "B[1,2]" = -1.0),
    K = 2,
    P = 2
  )
  expect_true(is.list(gd))
  gw <- .cluster_gating_weights(gd, x_row = c(1, 0))
  expect_equal(length(gw), 2)
  expect_equal(sum(gw), 1, tolerance = 1e-8)
  expect_null(.cluster_extract_gating_draw(c("foo" = 1), K = 2, P = 1))
  expect_null(.cluster_gating_weights(NULL, x_row = 1))

  spec <- list(
    meta = list(kernel = "normal", GPD = FALSE, P = 1, components = 2),
    plan = list(
      bulk = list(
        mean = list(mode = "link", link = "identity"),
        sd = list(mode = "fixed", value = 1)
      ),
      gpd = list()
    ),
    signatures = list(bulk = list(args = c("mean", "sd")))
  )
  dens_fun <- .cluster_resolve_density_fun(spec)
  expect_true(is.function(dens_fun))
  dens <- .cluster_component_density(
    spec = spec,
    draw_row = c("beta_mean[1,1]" = 0),
    k = 1L,
    x_row = 2,
    y_val = 0,
    density_fun = dens_fun
  )
  expect_true(is.finite(dens))
  expect_gt(dens, 0)
})

test_that("causal aggregation helpers cover conditional and marginal branches", {
  expect_null(.causal_ate_draws_matrix(NULL))
  expect_equal(dim(.causal_ate_draws_matrix(c(1, 2, 3))), c(3, 1))
  mat <- matrix(1:6, nrow = 2)
  expect_identical(.causal_ate_draws_matrix(mat), mat)
  expect_error(.causal_ate_draws_matrix(array(1:8, dim = c(2, 2, 2))), "Unexpected mean draw dimensions")

  fit_stub <- list(bundle = list(index = list(trt = c(2L, 4L, 4L))))
  expect_equal(.causal_effect_subset_index(fit_stub, n_pred = 1L, subset = "all"), 1L)
  expect_equal(.causal_effect_subset_index(fit_stub, n_pred = 5L, subset = "treated"), c(2L, 4L))
  expect_error(
    .causal_effect_subset_index(list(bundle = list(index = list(trt = integer(0)))), n_pred = 3L, subset = "treated"),
    "No treated rows"
  )

  summ <- .causal_summarize_draw_cols(matrix(c(1, 2, 3, 2, 3, 4), nrow = 3), compute_interval = TRUE, interval = "credible", level = 0.8)
  expect_equal(length(summ$estimate), 2)
  expect_true(all(is.finite(summ$lower)))
  expect_true(all(is.finite(summ$upper)))

  qobj <- list(
    trt = list(draws = matrix(c(1, 2, 3, 4, 2, 3, 4, 5), nrow = 4, byrow = TRUE)),
    con = list(draws = matrix(c(0, 1, 2, 3, 1, 2, 3, 4), nrow = 4, byrow = TRUE)),
    probs = c(0.25, 0.75),
    level = 0.9,
    interval = "credible",
    n_pred = 2L,
    meta = list(ps_enabled = FALSE)
  )
  qte_out <- .causal_aggregate_qte(qobj, idx = c(1L, 2L), effect_type = "qte")
  expect_s3_class(qte_out, "causalmixgpd_qte")
  expect_equal(nrow(qte_out$fit), 2)
  expect_true(all(c("estimate", "lower", "upper") %in% names(qte_out$fit)))
  expect_s3_class(qte_out$fit_df, "data.frame")

  qobj_uncond <- list(
    trt = list(draws = matrix(c(1, 2, 3, 4), nrow = 2)),
    con = list(draws = matrix(c(0, 1, 1, 2), nrow = 2)),
    probs = 0.5,
    level = 0.95,
    interval = "none",
    n_pred = 1L,
    meta = list(ps_enabled = FALSE)
  )
  qtt_out <- .causal_aggregate_qte(qobj_uncond, idx = 1L, effect_type = "qtt")
  expect_s3_class(qtt_out, "causalmixgpd_qte")
  expect_equal(qtt_out$type, "qtt")

  aobj <- list(
    trt = list(draws = matrix(c(2, 4, 6, 8), nrow = 2)),
    con = list(draws = matrix(c(1, 2, 3, 4), nrow = 2)),
    level = 0.9,
    interval = "credible",
    nsim_mean = 20L,
    meta = list(ps_enabled = FALSE)
  )
  ate_out <- .causal_aggregate_ate(aobj, idx = c(1L, 2L), effect_type = "ate")
  expect_s3_class(ate_out, "causalmixgpd_ate")
  expect_length(ate_out$fit, 1)
  expect_true(is.finite(ate_out$fit))
  expect_s3_class(ate_out$fit_df, "data.frame")
  expect_error(.causal_aggregate_ate(list(trt = list(draws = NULL), con = list(draws = NULL)), idx = 1L), "requires treated/control mean draws")
})

test_that("causal validation and bundle helpers cover conditional and input branches", {
  fit_cond <- .make_fake_causal_fit(has_X = TRUE, ps_enabled = FALSE)
  fit_uncond <- .make_fake_causal_fit(has_X = FALSE, ps_enabled = FALSE)

  expect_true(.causal_is_conditional_model(fit_cond))
  expect_false(.causal_is_conditional_model(fit_uncond))
  expect_silent(.causal_require_conditional(fit_cond, fn = "cate"))
  expect_error(.causal_require_conditional(fit_uncond, fn = "cate"), "cate\\(\\) is available only")
  expect_error(.causal_require_conditional(fit_uncond, fn = "cqte"), "cqte\\(\\) is available only")

  iv_none <- .causal_validate_interval(interval = "none", level = 0.9)
  expect_false(iv_none$compute_interval)
  expect_equal(iv_none$interval, "credible")
  expect_error(.causal_validate_interval(interval = "credible", level = 1), "'level' must be a numeric value between 0 and 1")

  expect_warning(.causal_warn_ignored_marginal_inputs("ate", newdata = matrix(1, ncol = 1), y = 1, conditional_fn = "cate"), "ignored")
  expect_false(.causal_warn_ignored_marginal_inputs("ate", conditional_fn = "cate"))

  expect_equal(.causal_resolve_conditional_x(fit_cond, fn = "cate"), fit_cond$bundle$data$X)
  expect_error(.causal_resolve_conditional_x(fit_uncond, fn = "cate"), "requires covariates")

  x <- cbind(x1 = c(-1, 0, 1, 2), x2 = c(0.5, 1.5, -0.5, 2.5))
  y <- abs(c(1.2, 0.8, 1.5, 2.2))
  A <- c(0L, 1L, 0L, 1L)
  cb <- build_causal_bundle(
    y = y,
    X = x,
    A = A,
    backend = c("sb", "crp"),
    kernel = c("normal", "gamma"),
    GPD = c(FALSE, TRUE),
    components = c(3, 4),
    PS = FALSE,
    monitor = "full",
    mcmc_outcome = mcmc_fast(seed = 5L)
  )
  expect_s3_class(cb, "causalmixgpd_causal_bundle")
  expect_true(cb$meta$monitor_policy$monitor_latent)
  expect_true(cb$meta$monitor_policy$monitor_v)
  expect_false(cb$meta$ps$enabled)

  cb_no_x <- build_causal_bundle(
    y = y,
    X = NULL,
    A = A,
    backend = "sb",
    kernel = "normal",
    components = 3,
    PS = TRUE,
    mcmc_outcome = mcmc_fast(seed = 6L)
  )
  expect_false(cb_no_x$meta$ps$enabled)

  cb_default_components <- build_causal_bundle(
    y = c(y, y),
    X = rbind(x, x),
    A = rep(A, 2L),
    backend = "sb",
    kernel = "normal",
    PS = FALSE,
    mcmc_outcome = mcmc_fast(seed = 7L)
  )
  expect_equal(cb_default_components$meta$components$con, 10L)
  expect_equal(cb_default_components$meta$components$trt, 10L)
  expect_equal(cb_default_components$outcome$con$spec$meta$components, 10L)
  expect_equal(cb_default_components$outcome$trt$spec$meta$components, 10L)

  expect_error(build_causal_bundle(y = numeric(0), X = x, A = A, backend = "sb", kernel = "normal", components = 3), "non-empty numeric vector")
  expect_error(build_causal_bundle(y = y, X = x[1:3, , drop = FALSE], A = A, backend = "sb", kernel = "normal", components = 3), "same number of rows")
  expect_error(build_causal_bundle(y = y, X = x, A = c(0L, 1L, 2L, 1L), backend = "sb", kernel = "normal", components = 3), "binary")
  expect_error(build_causal_bundle(y = y, X = x, A = rep(1L, 4), backend = "sb", kernel = "normal", components = 3), "Both treatment arms")
  expect_error(build_causal_bundle(y = y, X = x, A = A, backend = "sb", kernel = "normal", components = c(1, 3)), "treated")
  expect_error(build_causal_bundle(y = y, X = x, A = A, backend = "sb", kernel = "normal", components = c(3, 1)), "control")
  expect_error(build_causal_bundle(y = y, X = x, A = A, backend = "sb", kernel = "normal", components = 3, epsilon = c(1, 0.2)), "treated")
})

test_that("propensity score helper utilities cover bundle construction and prediction", {
  skip_if_not_installed("nimble")

  A <- c(0L, 1L, 0L, 1L)
  X <- cbind(x1 = c(-1, 0, 1, 2), x2 = c(0.5, 1.5, -0.5, 2.5))

  ps_logit <- .build_ps_bundle(
    A = A,
    X = X,
    spec = list(model = "logit", prior = list(mean = 0, sd = 1), include_intercept = TRUE),
    mcmc = mcmc_fast(seed = 1L)
  )
  expect_s3_class(ps_logit, "causalmixgpd_ps_bundle")
  expect_equal(ps_logit$spec$meta$type, "ps_logit")
  design <- .ps_design_matrix(ps_logit, X)
  expect_equal(dim(design), c(4, 3))
  expect_error(.ps_design_matrix(ps_logit, X[, "x1", drop = FALSE]), "do not match PS design")

  ps_probit <- .build_ps_bundle(
    A = A,
    X = X,
    spec = list(model = "probit", prior = list(mean = 0, sd = 1), include_intercept = FALSE),
    mcmc = mcmc_fast(seed = 2L)
  )
  expect_equal(ps_probit$spec$meta$type, "ps_probit")

  ps_naive <- .build_ps_bundle(
    A = A,
    X = X,
    spec = list(model = "naive", prior = list(mean = 0, sd = 1), include_intercept = FALSE),
    mcmc = mcmc_fast(seed = 3L)
  )
  expect_equal(ps_naive$spec$meta$type, "ps_naive")
  expect_error(
    .build_ps_bundle(A = A, X = X, spec = list(model = "bad", prior = list(mean = 0, sd = 1)), mcmc = mcmc_fast(seed = 4L)),
    "Unsupported PS model"
  )

  expect_equal(.apply_ps_scale(c(0.2, 0.8), scale = "prob"), c(0.2, 0.8))
  expect_equal(round(.apply_ps_scale(0.5, scale = "logit"), 6), 0)

  logit_fit <- list(mcmc = list(samples = coda::mcmc(matrix(
    c(0, 1, -1, 0.5, 0.5, -0.5),
    nrow = 2,
    byrow = TRUE,
    dimnames = list(NULL, c("beta[1]", "beta[2]", "beta[3]"))
  ))))
  ps_hat <- .compute_ps_from_fit(logit_fit, ps_logit, X, summary = "mean", clamp = 1e-6)
  expect_equal(length(ps_hat), nrow(X))
  expect_true(all(ps_hat > 0 & ps_hat < 1))

  probit_fit <- list(mcmc = list(samples = coda::mcmc(matrix(
    c(0, 0.5, -0.5, 0.25, 0.25, -0.25),
    nrow = 3,
    byrow = TRUE,
    dimnames = list(NULL, c("beta[1]", "beta[2]"))
  ))))
  probit_hat <- .compute_ps_from_fit(probit_fit, ps_probit, X, summary = "median", clamp = 1e-6)
  expect_equal(length(probit_hat), nrow(X))

  naive_fit <- list(mcmc = list(samples = coda::mcmc(matrix(
    c(
      0.5,
      -0.5, 0.5, 0.0, 1.0,
      1.0, 1.0, 1.0, 1.0
    ),
    nrow = 1,
    dimnames = list(NULL, c(
      "pi_prior",
      "mu[1,1]", "mu[2,1]", "mu[1,2]", "mu[2,2]",
      "sigma[1,1]", "sigma[2,1]", "sigma[1,2]", "sigma[2,2]"
    ))
  ))))
  naive_hat <- .compute_ps_from_fit(naive_fit, ps_naive, X, summary = "mean", clamp = 1e-6)
  expect_equal(length(naive_hat), nrow(X))
  expect_true(all(naive_hat > 0 & naive_hat < 1))
})

test_that("cluster label and psm S3 helpers cover summary and plotting branches", {
  lbl <- structure(
    list(
      labels = c(1L, 1L, 2L),
      components = 2L,
      source = "newdata",
      scores = matrix(c(0.9, 0.1, 0.8, 0.2, 0.25, 0.75), nrow = 3, byrow = TRUE)
    ),
    class = c("dpmixgpd_cluster_labels", "list")
  )
  lbl_sum <- summary(lbl)
  expect_s3_class(lbl_sum, "summary.dpmixgpd_cluster_labels")
  expect_equal(lbl_sum$n, 3)
  expect_silent(plot(lbl, type = "sizes"))
  expect_silent(plot(lbl, type = "certainty"))

  lbl_no_scores <- structure(list(labels = c(1L, 2L), components = 2L), class = c("dpmixgpd_cluster_labels", "list"))
  expect_warning(plot(lbl_no_scores, type = "certainty"), "No score matrix")

  psm_obj <- structure(
    list(psm = matrix(c(1, 0.2, 0.2, 1), 2), components = 2L, draw_index = 1L, psm_max_n = 2L),
    class = c("dpmixgpd_cluster_psm", "list")
  )
  psm_sum <- summary(psm_obj)
  expect_s3_class(psm_sum, "summary.dpmixgpd_cluster_psm")
  expect_equal(psm_sum$diagonal_mean, 1)
  expect_silent(plot(psm_obj, psm_max_n = 2L))
  expect_error(plot(psm_obj, psm_max_n = 1L), "PSM plot blocked")
  expect_error(plot(psm_obj, psm_max_n = 0L), "must be an integer >= 1")
})

test_that("cluster handcrafted fit covers newdata label prediction helpers", {
  skip_if_not_installed("coda")

  train <- data.frame(y = c(0.2, 2.8), x1 = c(0, 2))
  trm <- stats::terms(y ~ x1, data = train)
  mf <- stats::model.frame(trm, data = train)
  mm <- stats::model.matrix(stats::delete.response(trm), data = mf)
  formula_meta <- list(
    terms = trm,
    xlevels = stats::.getXlevels(trm, mf),
    contrasts = attr(mm, "contrasts"),
    X_cols = setdiff(colnames(mm), "(Intercept)"),
    response = "y"
  )

  samp <- coda::mcmc(matrix(
    c(
      1, 2, 0.5, 2.0, 0.0, 3.0,
      1, 2, 0.5, 2.0, 0.0, 3.0
    ),
    nrow = 2,
    byrow = TRUE,
    dimnames = list(NULL, c("z[1]", "z[2]", "eta[1]", "B[1,1]", "beta_mean[1,1]", "beta_mean[2,1]"))
  ))
  fit <- structure(
    list(
      spec = list(
        meta = list(kernel = "normal", GPD = FALSE, P = 1L, components = 2L),
        plan = list(
          bulk = list(
            mean = list(mode = "link", link = "identity"),
            sd = list(mode = "fixed", value = 1)
          ),
          gpd = list()
        ),
        signatures = list(bulk = list(args = c("mean", "sd"))),
        cluster = list(type = "weights", formula_meta = formula_meta)
      ),
      samples = coda::mcmc.list(samp),
      cache_env = new.env(parent = emptyenv())
    ),
    class = c("dpmixgpd_cluster_fit", "list")
  )

  pred <- predict_labels_newdata(fit, newdata = data.frame(y = c(0.1, 2.9), x1 = c(0, 2)))
  expect_equal(length(pred$labels), 2)
  expect_true(is.matrix(pred$scores))
  expect_equal(rowSums(pred$scores), rep(1, 2), tolerance = 1e-8)

  z_draws <- extract_z_draws(fit$samples)
  expect_equal(dim(z_draws), c(2, 2))
})

test_that("causal wrapper functions cover marginal branches with mocked outcome fits", {
  predict_rows <- list()
  testthat::local_mocked_bindings(
    predict = function(object, ...) {
      if (inherits(object, "fake_mixfit")) {
        call_args <- list(...)
        x_arg <- call_args$newdata %||% call_args$x %||% NULL
        predict_rows[[length(predict_rows) + 1L]] <<- if (is.null(x_arg)) NA_integer_ else nrow(as.matrix(x_arg))
        return(predict.fake_mixfit(object, ...))
      }
      stats::predict(object, ...)
    },
    .package = "CausalMixGPD"
  )

  fit_cond <- .make_fake_causal_fit(has_X = TRUE, ps_enabled = TRUE, ps_fit = TRUE)
  fit_uncond <- .make_fake_causal_fit(has_X = FALSE, ps_enabled = FALSE)

  qte_out <- NULL
  expect_warning(
    qte_out <- qte(fit_cond, probs = c(0.25, 0.75), newdata = matrix(1, ncol = 1), y = 1, show_progress = FALSE),
    "are ignored"
  )
  expect_s3_class(qte_out, "causalmixgpd_qte")

  qtt_out <- qtt(fit_cond, probs = c(0.25, 0.75), interval = "hpd", show_progress = FALSE)
  expect_s3_class(qtt_out, "causalmixgpd_qte")
  expect_equal(qtt_out$type, "qtt")
  expect_equal(predict_rows[[length(predict_rows) - 1L]], 2L)
  expect_equal(predict_rows[[length(predict_rows)]], 2L)

  ate_out <- ate(fit_cond, type = "mean", nsim_mean = 10L, show_progress = FALSE)
  att_out <- att(fit_cond, type = "rmean", cutoff = 3, nsim_mean = 10L, show_progress = FALSE)
  expect_s3_class(ate_out, "causalmixgpd_ate")
  expect_s3_class(att_out, "causalmixgpd_ate")
  expect_equal(att_out$type, "att")
  expect_true(is.na(ate_out$nsim_mean))
  expect_equal(att_out$nsim_mean, 10L)
  expect_s3_class(ate_out$fit_df, "data.frame")
  expect_s3_class(att_out$fit_df, "data.frame")
  expect_equal(predict_rows[[length(predict_rows) - 1L]], 2L)
  expect_equal(predict_rows[[length(predict_rows)]], 2L)
  expect_false(any(grepl("Posterior mean draws", utils::capture.output(print(ate_out)))))
  expect_true(any(grepl("Posterior mean draws", utils::capture.output(print(att_out)))))

  rm_cond <- ate_rmean(fit_cond, cutoff = 2, nsim_mean = 10L, show_progress = FALSE)
  rm_uncond <- ate_rmean(fit_uncond, cutoff = 2, nsim_mean = 10L, show_progress = FALSE)
  expect_s3_class(rm_cond, "causalmixgpd_ate")
  expect_s3_class(rm_uncond, "causalmixgpd_ate")
  expect_s3_class(rm_cond$fit_df, "data.frame")
  expect_s3_class(rm_uncond$fit_df, "data.frame")
})

test_that("conditional causal wrappers cover cate and cqte branches with mocked fits", {
  testthat::local_mocked_bindings(
    predict = function(object, ...) {
      if (inherits(object, "fake_mixfit")) {
        return(predict.fake_mixfit(object, ...))
      }
      stats::predict(object, ...)
    },
    .package = "CausalMixGPD"
  )

  fit_cond <- .make_fake_causal_fit(has_X = TRUE, ps_enabled = TRUE, ps_fit = TRUE)
  fit_no_ps <- .make_fake_causal_fit(has_X = TRUE, ps_enabled = TRUE, ps_fit = FALSE)
  fit_uncond <- .make_fake_causal_fit(has_X = FALSE, ps_enabled = FALSE)

  cate_out <- cate(fit_cond, type = "mean", nsim_mean = 10L, show_progress = FALSE)
  cate_rm <- cate(fit_cond, type = "rmean", cutoff = 2, interval = "hpd", nsim_mean = 10L, show_progress = FALSE)
  cqte_out <- cqte(fit_cond, probs = c(0.25, 0.75), show_progress = FALSE)
  expect_s3_class(cate_out, "causalmixgpd_ate")
  expect_s3_class(cate_rm, "causalmixgpd_ate")
  expect_s3_class(cqte_out, "causalmixgpd_qte")
  expect_equal(length(cate_out$fit), 3)
  expect_equal(dim(cqte_out$fit), c(3, 2))
  expect_s3_class(cate_out$fit_df, "data.frame")
  expect_s3_class(cate_rm$fit_df, "data.frame")
  expect_s3_class(cqte_out$fit_df, "data.frame")
  expect_true(is.na(cate_out$nsim_mean))
  expect_equal(cate_rm$nsim_mean, 10L)

  expect_warning(
    cate(fit_no_ps, type = "mean", nsim_mean = 10L, show_progress = FALSE),
    "missing PS model"
  )
  expect_warning(
    cqte(fit_no_ps, probs = c(0.25, 0.75), show_progress = FALSE),
    "missing PS model"
  )
  expect_error(cate(fit_uncond, show_progress = FALSE), "available only for conditional causal models")
  expect_error(cqte(fit_uncond, show_progress = FALSE), "available only for conditional causal models")
})

test_that("run_mcmc_causal orchestration covers PS, fallback, and validation branches", {
  x <- cbind(x1 = c(-1, 0, 1, 2), x2 = c(0.5, 1.5, -0.5, 2.5))
  y <- abs(c(1.2, 0.8, 1.5, 2.2))
  A <- c(0L, 1L, 0L, 1L)

  bundle_ps <- build_causal_bundle(
    y = y,
    X = x,
    A = A,
    backend = "sb",
    kernel = "normal",
    components = 3,
    PS = "logit",
    mcmc_outcome = mcmc_fast(seed = 12L),
    mcmc_ps = mcmc_fast(seed = 13L)
  )
  bundle_no_ps <- build_causal_bundle(
    y = y,
    X = x,
    A = A,
    backend = "sb",
    kernel = "normal",
    components = 3,
    PS = FALSE,
    mcmc_outcome = mcmc_fast(seed = 14L)
  )

  testthat::local_mocked_bindings(
    .run_ps_mcmc_bundle = function(bundle, ...) {
      list(mcmc = list(samples = matrix(c(0.1, 0.2, -0.1), nrow = 1, dimnames = list(NULL, c("beta[1]", "beta[2]", "beta[3]")))), timing = list(total = 0.1), bundle = bundle)
    },
    .compute_ps_from_fit = function(ps_fit, ps_bundle, X_new, ...) rep(0.4, nrow(as.matrix(X_new))),
    .apply_ps_scale = function(ps_prob, scale = c("prob", "logit"), clamp = 1e-6) as.numeric(ps_prob),
    build_code_from_spec = function(spec) list(code = "fake"),
    build_constants_from_spec = function(spec) list(N = 1L),
    build_monitors_from_spec = function(spec, ...) c("alpha"),
    run_mcmc_bundle_manual = function(bundle, ...) {
      list(samples = NULL, mcmc = list(samples = NULL), timing = list(mcmc = 0.2), bundle = bundle)
    },
    .package = "CausalMixGPD"
  )

  fit_ps <- run_mcmc_causal(bundle_ps, show_progress = FALSE, quiet = TRUE, timing = TRUE)
  fit_no_ps <- run_mcmc_causal(bundle_no_ps, show_progress = FALSE, quiet = TRUE, timing = TRUE)
  expect_s3_class(fit_ps, "causalmixgpd_causal_fit")
  expect_s3_class(fit_no_ps, "causalmixgpd_causal_fit")
  expect_equal(fit_ps$ps_hat, rep(0.4, 4))
  expect_equal(fit_ps$bundle$outcome$con$data$ps, c(0.4, 0.4))
  expect_null(fit_no_ps$ps_fit)
  expect_identical(fit_ps$bundle$outcome$con$code, bundle_ps$outcome$con$code)
  expect_identical(fit_ps$bundle$outcome$trt$code, bundle_ps$outcome$trt$code)
  expect_error(run_mcmc_causal(bundle_ps, z_update_every = 0L, show_progress = FALSE, quiet = TRUE), ">= 1")
})

test_that("cluster wrapper functions cover bundle-fit orchestration with mocked MCMC", {
  fake_cluster_fit <- structure(
    list(samples = NULL, mcmc = list(samples = NULL), timing = list(mcmc = 0.1)),
    class = "fake_cluster_fit"
  )

  testthat::local_mocked_bindings(
    run_cluster_mcmc = function(bundle, ...) {
      structure(
        list(bundle = bundle, spec = bundle$spec, samples = NULL, mcmc = list(samples = NULL), cache_env = new.env(parent = emptyenv())),
        class = c("dpmixgpd_cluster_fit", "list")
      )
    },
    run_mcmc_bundle_manual = function(bundle, ...) fake_cluster_fit,
    .package = "CausalMixGPD"
  )

  dat <- data.frame(y = abs(stats::rnorm(8)) + 0.1, x1 = stats::rnorm(8))
  b_mix <- build_cluster_bundle(y ~ x1, data = dat, kernel = "normal", GPD = FALSE, type = "weights", components = 3, mcmc = mcmc_fast(seed = 15L))
  b_gpd <- build_cluster_bundle(y ~ x1, data = dat, kernel = "normal", GPD = TRUE, type = "weights", components = 3, mcmc = mcmc_fast(seed = 16L))
  expect_s3_class(b_mix, "dpmixgpd_cluster_bundle")
  expect_s3_class(b_gpd, "dpmixgpd_cluster_bundle")

  fit1 <- dpmix.cluster(y ~ x1, data = dat, kernel = "normal", type = "weights", components = 3, mcmc = mcmc_fast(seed = 17L))
  fit2 <- dpmgpd.cluster(y ~ x1, data = dat, kernel = "normal", type = "weights", components = 3, mcmc = mcmc_fast(seed = 18L))
  expect_s3_class(fit1, "dpmixgpd_cluster_fit")
  expect_s3_class(fit2, "dpmixgpd_cluster_fit")
})

test_that("cluster bundle and fit S3 methods cover print summary and plot branches", {
  bundle <- structure(
    list(
      spec = list(
        meta = list(kernel = "normal", GPD = FALSE, N = 4L, P = 2L, components = 3L),
        cluster = list(type = "weights", param_link = TRUE)
      ),
      monitors = c("z[1]", "z[2]")
    ),
    class = c("dpmixgpd_cluster_bundle", "list")
  )

  expect_output(print(bundle), "Cluster bundle")
  sm_bundle <- summary(bundle)
  expect_s3_class(sm_bundle, "summary.dpmixgpd_cluster_bundle")
  expect_equal(sm_bundle$type, "weights")
  expect_silent(plot(bundle))

  fit <- structure(
    list(
      spec = bundle$spec,
      samples = matrix(0, nrow = 3, ncol = 3),
      cache_env = new.env(parent = emptyenv())
    ),
    class = c("dpmixgpd_cluster_fit", "list")
  )

  testthat::local_mocked_bindings(
    predict = function(object, type = c("label", "psm"), ...) {
      type <- match.arg(type)
      if (identical(type, "psm")) {
        return(structure(
          list(
            psm = matrix(c(1, 0.2, 0.2, 1), nrow = 2),
            labels = c(1L, 2L),
            components = 2L,
            draw_index = 1L,
            burnin = 0L,
            thin = 1L,
            psm_max_n = 2000L
          ),
          class = c("dpmixgpd_cluster_psm", "list")
        ))
      }
      structure(
        list(
          labels = c(1L, 1L, 2L),
          components = 2L,
          source = "train",
          burnin = 0L,
          thin = 1L
        ),
        class = c("dpmixgpd_cluster_labels", "list")
      )
    },
    extract_z_draws = function(samples, burnin = NULL, thin = NULL) {
      matrix(c(1L, 1L, 2L, 1L, 2L, 2L, 2L, 2L, 1L), nrow = 3, byrow = TRUE)
    },
    .package = "CausalMixGPD"
  )

  expect_output(print(fit), "Cluster fit")
  sm_fit <- summary(fit)
  expect_s3_class(sm_fit, "summary.dpmixgpd_cluster_fit")
  expect_equal(sm_fit$K_star, 2L)
  psm_out <- plot(fit, which = "psm", psm_max_n = 50L)
  k_out <- plot(fit, which = "k")
  lbl_out <- plot(fit, which = "sizes")
  expect_s3_class(psm_out, "ggplot")
  expect_s3_class(k_out, "ggplot")
  expect_s3_class(lbl_out, "ggplot")
})

test_that("predict.dpmixgpd_cluster_fit covers label psm newdata and validation", {
  fit <- structure(
    list(
      spec = list(meta = list()),
      samples = "fake_samples",
      cache_env = new.env(parent = emptyenv())
    ),
    class = c("dpmixgpd_cluster_fit", "list")
  )

  testthat::local_mocked_bindings(
    .cluster_samples_to_matrix = function(samples) {
      matrix(
        c(1, 1, 2, 1, 2, 2),
        nrow = 2,
        byrow = TRUE,
        dimnames = list(NULL, c("z[1]", "z[2]", "z[3]"))
      )
    },
    .cluster_draw_indices = function(n_draws, burnin = NULL, thin = NULL) seq_len(n_draws),
    .cluster_extract_z_from_matrix = function(draw_sub) {
      matrix(c(1L, 1L, 2L, 1L, 2L, 2L), nrow = 2, byrow = TRUE)
    },
    compute_psm = function(z_draws) {
      matrix(
        c(1, 0.2, 0.1, 0.2, 1, 0.3, 0.1, 0.3, 1),
        nrow = 3,
        byrow = TRUE
      )
    },
    dahl_labels = function(z_draws, psm) list(labels = c(1L, 2L, 2L), K = 2L, draw_index = 1L),
    .cluster_compute_scores = function(z_draws, labels, psm) {
      matrix(c(0.9, 0.1, 0.2, 0.8, 0.1, 0.9), nrow = 3, byrow = TRUE)
    },
    predict_labels_newdata = function(fit, newdata, burnin = NULL, thin = NULL) {
      list(
        labels = c(1L, 2L),
        scores = matrix(c(0.7, 0.3, 0.4, 0.6), nrow = 2, byrow = TRUE),
        K = 2L
      )
    },
    .package = "CausalMixGPD"
  )

  train_lbl <- predict(fit, type = "label", return_scores = TRUE)
  train_lbl_cached <- predict(fit, type = "label", return_scores = FALSE)
  psm <- predict(fit, type = "psm", psm_max_n = 10L)
  nd_lbl <- predict(fit, newdata = data.frame(y = c(0.1, 0.2)), type = "label", return_scores = TRUE)

  expect_s3_class(train_lbl, "dpmixgpd_cluster_labels")
  expect_s3_class(train_lbl_cached, "dpmixgpd_cluster_labels")
  expect_true(is.matrix(train_lbl$scores))
  expect_s3_class(psm, "dpmixgpd_cluster_psm")
  expect_s3_class(nd_lbl, "dpmixgpd_cluster_labels")
  expect_equal(nd_lbl$source, "newdata")

  expect_error(predict(fit, type = "label", psm_max_n = 0L), "integer >= 1")
  expect_error(
    predict(fit, newdata = data.frame(y = 1), type = "psm"),
    "only available for training data"
  )
  expect_error(
    predict(fit, type = "psm", psm_max_n = 2L),
    "PSM is O\\(n\\^2\\)"
  )
})

test_that("causal prediction wrapper covers quantile, density, survival, prob, and validation branches", {
  testthat::local_mocked_bindings(
    predict = function(object, ...) {
      if (inherits(object, "fake_mixfit")) {
        return(predict.fake_mixfit(object, ...))
      }
      stats::predict(object, ...)
    },
    .package = "CausalMixGPD"
  )

  fit_ps <- .make_fake_causal_fit(has_X = TRUE, ps_enabled = TRUE, ps_fit = TRUE)
  fit_no_ps_model <- .make_fake_causal_fit(has_X = TRUE, ps_enabled = TRUE, ps_fit = FALSE)
  fit_nodraw <- .make_fake_causal_fit(has_X = TRUE, ps_enabled = FALSE, no_quantile_draws = TRUE)

  x_new <- cbind(x1 = c(0, 1), x2 = c(1, 2))
  dens <- predict(fit_ps, newdata = x_new, y = c(0.1, 0.2), type = "density", interval = "credible", show_progress = FALSE)
  surv <- predict(fit_ps, newdata = x_new, y = c(0.1, 0.2), type = "survival", interval = NULL, show_progress = FALSE)
  prob <- predict(fit_ps, newdata = x_new, y = c(0.1, 0.2), type = "prob", interval = NULL, show_progress = FALSE)
  expect_s3_class(dens, "causalmixgpd_causal_predict")
  expect_s3_class(surv, "causalmixgpd_causal_predict")
  expect_s3_class(prob, "causalmixgpd_causal_predict")
  expect_true(all(c("trt_estimate", "con_estimate") %in% names(dens)))

  qdraw <- predict(fit_ps, newdata = x_new, type = "quantile", p = c(0.25, 0.75), show_progress = FALSE)
  qnodraw <- predict(fit_nodraw, newdata = x_new, type = "quantile", p = c(0.25, 0.75), show_progress = FALSE)
  qsingle <- predict(fit_ps, newdata = x_new, type = "quantile", p = 0.5, interval = "none", show_progress = FALSE)
  mean_out <- predict(fit_ps, newdata = x_new, type = "mean", ps = c(0.2, 0.8), show_progress = FALSE)
  expect_s3_class(qdraw, "causalmixgpd_causal_predict")
  expect_s3_class(qnodraw, "causalmixgpd_causal_predict")
  expect_s3_class(qsingle, "causalmixgpd_causal_predict")
  expect_s3_class(mean_out, "causalmixgpd_causal_predict")
  expect_true(all(c("index", "estimate") %in% names(qdraw)))

  expect_error(predict(fit_ps, newdata = x_new, type = "quantile", p = c(0, 0.5), show_progress = FALSE), "must be in \\(0, 1\\)")
  expect_error(predict(fit_ps, newdata = x_new, type = "density", show_progress = FALSE), "requires 'y'")
  expect_error(predict(fit_ps, newdata = x_new, y = 1, type = "density", show_progress = FALSE), "Length of 'y' must match")
  expect_error(predict(fit_ps, newdata = x_new, ps = 0.5, type = "mean", show_progress = FALSE), "Length of 'ps' must equal")
  expect_error(
    predict(fit_no_ps_model, newdata = x_new, type = "mean", show_progress = FALSE),
    "missing PS model"
  )
})

test_that("causal survival prediction collapses duplicated long-form arm output by id and y", {
  testthat::local_mocked_bindings(
    predict = function(object, ...) {
      if (inherits(object, "fake_mixfit")) {
        args <- list(...)
        id_vals <- args$id
        y_vals <- as.numeric(args$y)
        newdata <- args$newdata
        type <- match.arg(args$type, c("density", "survival"))
        arm_shift <- if (identical(object$arm, "trt")) 1 else 0
        if (is.null(id_vals)) {
          id_vals <- seq_len(nrow(as.matrix(newdata)))
        }
        fit <- do.call(rbind, lapply(seq_along(id_vals), function(i) {
          data.frame(
            id = rep(id_vals[i], length(y_vals)),
            y = y_vals,
            survival = arm_shift + 0.1 * i,
            lower = arm_shift + 0.1 * i - 0.01,
            upper = arm_shift + 0.1 * i + 0.01
          )
        }))
        return(list(fit = fit))
      }
      stats::predict(object, ...)
    },
    .package = "CausalMixGPD"
  )

  fit_ps <- .make_fake_causal_fit(has_X = TRUE, ps_enabled = FALSE)
  x_new <- cbind(x1 = c(0, 1), x2 = c(1, 2))
  y_new <- c(0.1, 0.2)
  surv <- predict(fit_ps, newdata = x_new, y = y_new, id = c(10L, 20L), type = "survival", show_progress = FALSE)

  expect_s3_class(surv, "causalmixgpd_causal_predict")
  expect_equal(nrow(surv), 2L)
  expect_equal(surv$id, c(10L, 20L))
  expect_equal(surv$y, y_new)
  expect_equal(surv$trt_estimate, c(1.1, 1.2))
  expect_equal(surv$con_estimate, c(0.1, 0.2))
})

test_that("causal prediction sample branch exposes long-form data frames", {
  testthat::local_mocked_bindings(
    .extract_draws_matrix = function(object, ...) matrix(1:6, nrow = 3L),
    .predict_mixgpd = function(object, x = NULL, id = NULL, type = "sample", nsim = NULL, ...) {
      n_pred <- if (!is.null(x)) nrow(as.matrix(x)) else 1L
      id_vals <- if (!is.null(id) && length(id) == n_pred) id else seq_len(n_pred)
      fit <- if (n_pred == 1L) {
        c(1.1, 1.2, 1.3)
      } else {
        matrix(seq_len(n_pred * 3L) / 10, nrow = n_pred, ncol = 3L)
      }
      list(
        fit = fit,
        fit_df = .values_to_long_df(fit, id = id_vals, value_name = "sample"),
        type = type
      )
    },
    .package = "CausalMixGPD"
  )

  fit_ps <- .make_fake_causal_fit(has_X = TRUE, ps_enabled = TRUE, ps_fit = TRUE)
  x_new <- cbind(x1 = c(0, 1), x2 = c(1, 2))
  out <- predict(fit_ps, newdata = x_new, type = "sample", nsim = 3L, show_progress = FALSE)

  expect_s3_class(out, "causalmixgpd_causal_predict")
  expect_s3_class(out$fit_df, "data.frame")
  expect_s3_class(out$trt_fit_df, "data.frame")
  expect_s3_class(out$con_fit_df, "data.frame")
  expect_true(all(c("id", "draw", "sample") %in% names(out$fit_df)))
})

# ===== END unit/test-causal-and-cluster-helper-coverage.R =====
