test_that("prepare_dcvar_covariate_data aligns covariates after sorting and outcome filtering", {
  df <- data.frame(
    time = c(4, 1, 3, 2),
    y1 = c(40, 10, NA, 20),
    y2 = c(44, 11, 33, 22),
    phase = c(4, 1, 3, 2)
  )

  expect_warning(
    out <- prepare_dcvar_covariate_data(
      df,
      vars = c("y1", "y2"),
      covariates = "phase",
      standardize = FALSE,
      allow_gaps = TRUE
    ),
    "Removing 1 row"
  )

  expect_equal(out$n_time, 3)
  expect_equal(as.numeric(out$Y[, 1]), c(10, 20, 40))
  expect_equal(as.numeric(out$X[, 1]), c(1, 2, 4))
  expect_equal(attr(out, "time_values"), c(1, 2, 4))
  expect_equal(attr(out, "covariates"), "phase")
})

test_that("prepare_dcvar_covariate_data validates covariates", {
  df <- data.frame(
    time = 1:5,
    y1 = rnorm(5),
    y2 = rnorm(5),
    phase = c(0, 0, 1, 1, 1),
    label = letters[1:5],
    missing_x = c(0, 1, NA, 1, 0),
    constant_x = 1
  )

  expect_error(
    prepare_dcvar_covariate_data(df, vars = c("y1", "y2"), covariates = "missing_col"),
    "not found"
  )
  expect_error(
    prepare_dcvar_covariate_data(df, vars = c("y1", "y2"), covariates = "label"),
    "must be numeric"
  )
  expect_error(
    prepare_dcvar_covariate_data(df, vars = c("y1", "y2"), covariates = "missing_x"),
    "complete finite"
  )
  expect_error(
    prepare_dcvar_covariate_data(
      df,
      vars = c("y1", "y2"),
      covariates = "constant_x",
      standardize_covariates = TRUE
    ),
    "zero"
  )
})

test_that("prepare_dcvar_covariate_data allows binary phase indicators by default", {
  df <- data.frame(
    time = 1:6,
    y1 = rnorm(6),
    y2 = rnorm(6),
    phase = c(0, 0, 0, 1, 1, 1)
  )

  out <- prepare_dcvar_covariate_data(df, vars = c("y1", "y2"), covariates = "phase")

  expect_equal(as.numeric(out$X[, 1]), df$phase)
  expect_false(attr(out, "standardized_covariates"))
  expect_equal(out$P, 1)
})

test_that("covariate init preserves Stan vector dimensions when length is one", {
  init <- .init_dcvar_covariate_params(D = 2, T_obs = 3, P = 1, drift = TRUE)

  expect_equal(dim(init$beta), 1L)
  expect_equal(dim(init$omega_raw), 1L)
})

test_that("dcvar_stan_path exposes covariate Stan models", {
  drift_path <- dcvar_stan_path("dcvar_covariate")
  nodrift_path <- dcvar_stan_path("dcvar_covariate_nodrift")

  expect_true(file.exists(drift_path))
  expect_true(file.exists(nodrift_path))
  # The public path helper returns the legacy-compatible Stan files that match
  # prepare_dcvar_covariate_data(); dcvar_covariate() routes bundled drift fits
  # to dcvar_dynamic.stan internally.
  expect_match(basename(drift_path), "^dcvar_covariate_ncp\\.stan$")
  expect_match(basename(nodrift_path), "^dcvar_covariate_nodrift\\.stan$")
  expect_error(dcvar_stan_path("dcvar_covariate", margins = "gamma"), "normal")
})

make_covariate_stub_fit <- function(drift = TRUE) {
  variables <- c(
    "mu[1]", "mu[2]",
    "Phi[1,1]", "Phi[2,1]", "Phi[1,2]", "Phi[2,2]",
    "sigma_eps[1]", "sigma_eps[2]",
    "beta_0", "beta[1]", "beta[2]",
    paste0("rho[", 1:4, "]"),
    paste0("log_lik[", 1:4, "]")
  )
  if (drift) {
    variables <- c(variables, "sigma_omega")
  }

  draws <- array(
    rnorm(40 * length(variables), 0, 0.1),
    dim = c(40, 1, length(variables)),
    dimnames = list(NULL, NULL, variables)
  )
  rho_cols <- match(paste0("rho[", 1:4, "]"), variables)
  draws[, , rho_cols] <- tanh(draws[, , rho_cols])
  log_lik_cols <- match(paste0("log_lik[", 1:4, "]"), variables)
  draws[, , log_lik_cols] <- -abs(draws[, , log_lik_cols])

  stan_data <- structure(
    list(n_time = 5, D = 2, P = 2),
    time_values = 101:105
  )

  new_dcvar_covariate_fit(
    fit = posterior::as_draws_array(draws),
    stan_data = stan_data,
    vars = c("y1", "y2"),
    covariates = c("phase", "load"),
    standardized = TRUE,
    standardized_covariates = FALSE,
    drift = drift,
    zero_init_eta = TRUE,
    backend = "rstan",
    priors = list(),
    meta = list(chains = 1, iter_warmup = 10, iter_sampling = 40)
  )
}

test_that("covariate fit extractors work for drift and no-drift objects", {
  fit <- make_covariate_stub_fit(drift = TRUE)
  no_drift_fit <- make_covariate_stub_fit(drift = FALSE)

  expect_s3_class(fit, "dcvar_covariate_fit")
  expect_equal(fit$model, "dcvar_covariate")
  expect_equal(no_drift_fit$model, "dcvar_covariate_nodrift")

  co <- coef(fit)
  expect_named(co, c("mu", "Phi", "sigma_eps", "beta_0", "beta", "sigma_omega"))
  expect_equal(names(co$beta), c("phase", "load"))

  co_no_drift <- coef(no_drift_fit)
  expect_named(co_no_drift, c("mu", "Phi", "sigma_eps", "beta_0", "beta"))

  rho_df <- rho_trajectory(fit)
  expect_equal(rho_df$time, 102:105)
  expect_true(all(rho_df$mean >= -1 & rho_df$mean <= 1))

  effects <- covariate_effects(fit)
  expect_true(all(c("(Intercept)", "phase", "load") %in% effects$term))
  expect_false("sigma_omega" %in% effects$term)

  effects_no_drift <- covariate_effects(no_drift_fit)
  expect_false("sigma_omega" %in% effects_no_drift$term)
})

test_that("loo works for covariate fits with log_lik", {
  fit <- make_covariate_stub_fit(drift = TRUE)

  out <- suppressWarnings(loo::loo(fit))

  expect_s3_class(out, "loo")
})

test_that("dcvar_covariate fits end to end (drift and no-drift)", {
  skip_if_no_rstan()

  fit <- get_covariate_fit(drift = TRUE)
  expect_s3_class(fit, "dcvar_covariate_fit")
  eff <- covariate_effects(fit)
  expect_true(is.data.frame(eff))
  traj <- rho_trajectory(fit)
  expect_true(all(is.finite(traj$mean)))
  expect_true(all(traj$mean > -1 & traj$mean < 1))

  fit_nodrift <- get_covariate_fit(drift = FALSE)
  expect_s3_class(fit_nodrift, "dcvar_covariate_fit")
  traj_nodrift <- rho_trajectory(fit_nodrift)
  expect_true(all(is.finite(traj_nodrift$mean)))
})
