# =============================================================================
# test-integration-assoc.R — Integration tests for assoc_ series
# Uses a larger simulated dataset (n=2000) to test real model behaviour.
# Run manually before release: devtools::test(filter = "integration-assoc")
# =============================================================================

# ===========================================================================
# Shared fixture (n=2000, realistic UKB-like distributions)
# ===========================================================================

skip_on_cran()
RNGkind("Mersenne-Twister", "Inversion", "Rejection")
set.seed(2024L)
N <- 2000L

DT <- data.table::data.table(
  eid                = seq_len(N),
  followup_years     = round(runif(N, 0.5, 15), 2),
  copd_01            = rbinom(N, 1, 0.08),
  t2d_01              = rbinom(N, 1, 0.15),
  t2d_severity        = factor(
    sample(c("No AD", "Mild", "Moderate/Severe"), N, TRUE, c(0.70, 0.20, 0.10)),
    levels = c("No AD", "Mild", "Moderate/Severe")
  ),
  bmi_cat            = factor(
    sample(c("Normal", "Overweight", "Obese"), N, TRUE, c(0.40, 0.35, 0.25)),
    levels = c("Normal", "Overweight", "Obese")
  ),
  bmi_num            = round(rnorm(N, 27, 5), 1),
  age_at_recruitment = round(rnorm(N, 57, 8), 1),
  sex                = factor(sample(c("Male", "Female"), N, TRUE)),
  tdi                = rnorm(N, -1, 3),
  smoking            = factor(sample(c("Never", "Previous", "Current"), N, TRUE)),
  ethnicity          = factor(sample(c("White", "Asian", "Black", "Other"),
                                     N, TRUE, c(0.85, 0.07, 0.04, 0.04)))
)
# Inject NA into tdi to test listwise deletion
DT[sample(N, 150L), tdi := NA_real_]

# Competing risks columns
set.seed(2025L)
DT[, death_01      := rbinom(N, 1, 0.05)]
DT[copd_01 == 1L & death_01 == 1L, death_01 := 0L]
DT[, censoring_type := data.table::fcase(
  copd_01  == 1L, 1L,
  death_01 == 1L, 2L,
  default  = 0L
)]

COVS_FULL <- c("tdi", "smoking", "ethnicity")


# ===========================================================================
# assoc_coxph() — integration
# ===========================================================================

test_that("assoc_coxph() runs on n=2000 without error", {
  expect_no_error(suppressMessages(
    assoc_coxph(DT, "copd_01", "followup_years", "t2d_01",
                covariates = COVS_FULL)
  ))
})

test_that("assoc_coxph() 3-model output is numerically coherent", {
  res <- suppressMessages(
    assoc_coxph(DT, "copd_01", "followup_years", "t2d_01",
                covariates = COVS_FULL)
  )
  expect_equal(uniqueN(res$model), 3L)
  expect_true(all(res$HR > 0))
  expect_true(all(res$CI_lower < res$HR))
  expect_true(all(res$HR < res$CI_upper))
  expect_true(all(res$p_value >= 0 & res$p_value <= 1))
})

test_that("assoc_coxph() factor exposure returns n_levels - 1 terms per model", {
  res <- suppressMessages(
    assoc_coxph(DT, "copd_01", "followup_years", "bmi_cat")
  )
  # Drop empty factor levels before counting (ordered factor may retain all levels)
  per_model <- table(droplevels(res$model))
  expect_true(all(per_model == 2L))   # 3 levels → 2 non-reference terms
})

test_that("assoc_coxph() HR_label format matches 'x.xx (x.xx-x.xx)'", {
  res <- suppressMessages(
    assoc_coxph(DT, "copd_01", "followup_years", "t2d_01")
  )
  expect_true(all(grepl("^\\d+\\.\\d+ \\(\\d+\\.\\d+", res$HR_label)))
})

test_that("assoc_coxph() person_years is positive and finite", {
  res <- suppressMessages(
    assoc_coxph(DT, "copd_01", "followup_years", "t2d_01")
  )
  expect_true(all(res$person_years > 0))
  expect_true(all(is.finite(res$person_years)))
})

test_that("assoc_coxph() n_events consistent with data", {
  res <- suppressMessages(
    assoc_coxph(DT, "copd_01", "followup_years", "t2d_01")
  )
  # Unadjusted model uses all rows (no covariate NA)
  expected_events <- sum(DT$copd_01)
  expect_equal(res[res$model == "Unadjusted", "n_events"][[1L]], expected_events)
})

test_that("assoc_coxph() multiple exposures correct row count", {
  res <- suppressMessages(
    assoc_coxph(DT, "copd_01", "followup_years", c("t2d_01", "bmi_num", "bmi_cat"))
  )
  # t2d_01: 1 term, bmi_num: 1 term, bmi_cat: 2 terms → 4 per model × 2 models = 8
  expect_equal(nrow(res), 8L)
})


# ===========================================================================
# assoc_logistic() — integration
# ===========================================================================

test_that("assoc_logistic() runs on n=2000 without error", {
  expect_no_error(suppressMessages(
    assoc_logistic(DT, "copd_01", "t2d_01", covariates = COVS_FULL)
  ))
})

test_that("assoc_logistic() OR > 0, CI spans OR, p in [0,1]", {
  res <- suppressMessages(
    assoc_logistic(DT, "copd_01", "t2d_01", covariates = COVS_FULL)
  )
  expect_true(all(res$OR > 0))
  expect_true(all(res$CI_lower < res$OR & res$OR < res$CI_upper))
  expect_true(all(res$p_value >= 0 & res$p_value <= 1))
})


# ===========================================================================
# assoc_linear() — integration
# ===========================================================================

test_that("assoc_linear() runs on n=2000 without error", {
  expect_no_error(suppressMessages(
    assoc_linear(DT, "bmi_num", "t2d_01", covariates = COVS_FULL)
  ))
})

test_that("assoc_linear() se > 0 and p in [0,1]", {
  res <- suppressMessages(
    assoc_linear(DT, "bmi_num", "t2d_01", covariates = COVS_FULL)
  )
  expect_true(all(res$se > 0))
  expect_true(all(res$p_value >= 0 & res$p_value <= 1))
})


# ===========================================================================
# assoc_coxph_zph() — integration
# ===========================================================================

test_that("assoc_coxph_zph() runs without error and returns ph_satisfied", {
  res <- suppressMessages(
    assoc_coxph_zph(DT, "copd_01", "followup_years", "t2d_01")
  )
  expect_true("ph_satisfied" %in% names(res))
  expect_true(is.logical(res$ph_satisfied))
})


# ===========================================================================
# assoc_subgroup() — integration
# ===========================================================================

test_that("assoc_subgroup() runs on n=2000 by sex without error", {
  expect_no_error(suppressMessages(
    assoc_subgroup(DT, "copd_01", "followup_years", "t2d_01",
                   by = "sex", covariates = COVS_FULL)
  ))
})

test_that("assoc_subgroup() p_interaction is numeric scalar in [0,1]", {
  res <- suppressMessages(
    assoc_subgroup(DT, "copd_01", "followup_years", "t2d_01",
                   by = "sex", covariates = COVS_FULL)
  )
  pi <- unique(stats::na.omit(res$p_interaction))
  expect_true(length(pi) >= 1L)
  expect_true(all(pi >= 0 & pi <= 1))
})

test_that("assoc_subgroup() subgroup n sums to <= total n", {
  res <- suppressMessages(
    assoc_subgroup(DT, "copd_01", "followup_years", "t2d_01", by = "sex")
  )
  total_n <- sum(res[res$model == "Unadjusted", "n"][[1L]])
  expect_lte(total_n, nrow(DT))
})

test_that("assoc_subgroup() works with 3-level by variable", {
  res <- suppressMessages(
    assoc_subgroup(DT, "copd_01", "followup_years", "t2d_01", by = "smoking")
  )
  expect_equal(uniqueN(res$subgroup_level), 3L)
})


# ===========================================================================
# assoc_trend() — integration
# ===========================================================================

test_that("assoc_trend() coxph on n=2000 returns reference row + non-ref rows", {
  res <- suppressMessages(
    assoc_trend(DT, "copd_01", "followup_years", "t2d_severity",
                method = "coxph")
  )
  ref_rows <- res[res$level == "No AD" & res$model == "Unadjusted", ]
  expect_equal(nrow(ref_rows), 1L)
  expect_equal(ref_rows$HR[1L], 1.0)
  expect_true(is.na(ref_rows$CI_lower[1L]))
})

test_that("assoc_trend() p_trend is numeric and in [0,1]", {
  res <- suppressMessages(
    assoc_trend(DT, "copd_01", "followup_years", "t2d_severity",
                method = "coxph")
  )
  pt <- stats::na.omit(unique(res$p_trend))
  expect_true(all(pt >= 0 & pt <= 1))
})

test_that("assoc_trend() logistic method returns OR_per_score", {
  res <- suppressMessages(
    assoc_trend(DT, "copd_01", "followup_years", "t2d_severity",
                method = "logistic")
  )
  expect_true("OR_per_score" %in% names(res))
})

test_that("assoc_trend() linear method returns beta_per_score", {
  res <- suppressMessages(
    assoc_trend(DT, "bmi_num", "followup_years", "t2d_severity",
                method = "linear")
  )
  expect_true("beta_per_score" %in% names(res))
})


# ===========================================================================
# assoc_competing() — integration
# ===========================================================================

test_that("assoc_competing() Mode A runs on n=2000 without error", {
  expect_no_error(suppressMessages(
    assoc_competing(DT, "censoring_type", "followup_years", "t2d_01",
                    event_val = 1L, compete_val = 2L,
                    covariates = COVS_FULL)
  ))
})

test_that("assoc_competing() Mode B runs without error", {
  expect_no_error(suppressMessages(
    assoc_competing(DT, "copd_01", "followup_years", "t2d_01",
                    compete_col = "death_01",
                    covariates  = COVS_FULL)
  ))
})

test_that("assoc_competing() SHR > 0, CI spans SHR, p in [0,1]", {
  res <- suppressMessages(
    assoc_competing(DT, "censoring_type", "followup_years", "t2d_01",
                    event_val = 1L, compete_val = 2L,
                    covariates = COVS_FULL)
  )
  expect_true(all(res$SHR > 0))
  expect_true(all(res$CI_lower < res$SHR & res$SHR < res$CI_upper))
  expect_true(all(res$p_value >= 0 & res$p_value <= 1))
})

test_that("assoc_competing() n_compete matches expected competing event count", {
  res <- suppressMessages(
    assoc_competing(DT, "censoring_type", "followup_years", "t2d_01",
                    event_val = 1L, compete_val = 2L)
  )
  expected_compete <- sum(DT$death_01)
  # n_compete in model may be <= raw count (complete cases only)
  expect_lte(res[res$model == "Unadjusted", "n_compete"][[1L]], expected_compete + 1L)
})

test_that("assoc_competing() Fully adjusted n <= Unadjusted n", {
  res <- suppressMessages(
    assoc_competing(DT, "censoring_type", "followup_years", "t2d_01",
                    event_val = 1L, compete_val = 2L,
                    covariates = COVS_FULL)
  )
  n_full  <- res[res$model == "Fully adjusted", "n"][[1L]]
  n_unadj <- res[res$model == "Unadjusted",     "n"][[1L]]
  expect_lte(n_full, n_unadj)
})

test_that("assoc_competing() factor exposure produces correct term names", {
  res <- suppressMessages(
    assoc_competing(DT, "censoring_type", "followup_years", "bmi_cat",
                    event_val = 1L, compete_val = 2L)
  )
  expect_true("bmi_catOverweight" %in% res$term)
  expect_true("bmi_catObese"      %in% res$term)
})


# ===========================================================================
# assoc_lag() — integration
# ===========================================================================

test_that("assoc_lag() runs on n=2000 with lag=c(0,1,2) without error", {
  expect_no_error(suppressMessages(
    assoc_lag(DT, "copd_01", "followup_years", "t2d_01",
              lag_years  = c(0, 1, 2),
              covariates = COVS_FULL)
  ))
})

test_that("assoc_lag() lag=0 n equals total cohort size", {
  res <- suppressMessages(
    assoc_lag(DT, "copd_01", "followup_years", "t2d_01",
              lag_years = c(0, 2))
  )
  n_lag0 <- res[res$lag_years == 0 & res$model == "Unadjusted", "n"][[1L]]
  expect_equal(n_lag0, nrow(DT))
})

test_that("assoc_lag() HR positive and CI ordered for all lags", {
  res <- suppressMessages(
    assoc_lag(DT, "copd_01", "followup_years", "t2d_01",
              lag_years = c(1, 2, 5))
  )
  expect_true(all(res$HR > 0))
  expect_true(all(res$CI_lower < res$HR & res$HR < res$CI_upper))
})

test_that("assoc_lag() n_excluded monotonically increases with lag", {
  res <- suppressMessages(
    assoc_lag(DT, "copd_01", "followup_years", "t2d_01",
              lag_years = c(1, 2, 5))
  )
  excl <- unique(res[, c("lag_years", "n_excluded")])
  data.table::setorder(excl, lag_years)
  expect_true(all(diff(excl$n_excluded) >= 0))
})

test_that("assoc_lag() 3 lags × 2 exposures × 3 models = 18 rows", {
  res <- suppressMessages(
    assoc_lag(DT, "copd_01", "followup_years",
              exposure_col = c("t2d_01", "bmi_num"),
              lag_years    = c(1, 2, 5),
              covariates   = COVS_FULL)
  )
  expect_equal(nrow(res), 18L)
})

test_that("assoc_lag() column order: lag_years directly after model", {
  res <- suppressMessages(
    assoc_lag(DT, "copd_01", "followup_years", "t2d_01", lag_years = 2)
  )
  pos_model    <- which(names(res) == "model")
  pos_lag      <- which(names(res) == "lag_years")
  pos_excluded <- which(names(res) == "n_excluded")
  expect_equal(pos_lag,      pos_model + 1L)
  expect_equal(pos_excluded, pos_model + 2L)
})
