# Regression tests for the bug-fix batches (discrete bugs, NA-safe filtering, grep
# validation). Each test would fail against the code as it was before the fix.

# --- dw_find_col: validated column lookup ----------------------------------

test_that("dw_find_col returns the single match, or errors clearly", {
  nms <- c("ID", "gene_symbol", "logFC", "FDR", "PValue")
  expect_equal(dw_find_col(nms, "^fdr$", "FDR"), "FDR")
  expect_equal(dw_find_col(nms, "^logfc$|fold$", "log fold-change"), "logFC")
  # zero matches -> clear error rather than a cryptic x[[character(0)]] failure
  expect_error(dw_find_col(nms, "entrez", "Entrez"), "Could not find")
  # several matches -> clear error rather than x[[c('a','b')]]
  expect_error(
    dw_find_col(c("FDR", "adj.P.Val"), "^fdr$|^adj*\\.{0,1}p\\.{0,1}val[e-u]{0,2}$", "FDR"),
    "several"
  )
})

# --- NA-safe filtering in the cleaned-table writer -------------------------

test_that("diffr_expr_generate_cleaned_de_table_output never emits NA rows", {
  quiet_log()
  out.dir <- tempfile("dw_clean_"); dir.create(out.dir)
  on.exit(unlink(out.dir, recursive = TRUE), add = TRUE)
  key <- data.frame(group = rep(c("control", "treated"), each = 4),
                    row.names = paste0("S", 1:8))
  # two rows are individually significant on one axis but NA on the other:
  # a pre-fix logical subset would turn them into all-NA output rows
  d3 <- data.frame(ID = paste0("g", 1:10), gene_symbol = paste0("G", 1:10),
                   logFC = c(rnorm(8, 0, 3), NA, 3),
                   FDR   = c(runif(8, 0, 0.001), 0.001, NA),
                   stringsAsFactors = FALSE)

  diffr_expr_generate_cleaned_de_table_output(
    contrast = "treated-control", annotated.normcnt = d3,
    samp.name.and.group.key = key, out.dir = out.dir,
    analysis.name = "t", filtered.lists = TRUE, fdr.thr = 0.05, logfc.thr = 1)

  f <- list.files(out.dir, pattern = "clean\\.tsv$", full.names = TRUE)
  expect_length(f, 1L)
  res <- read.delim(f, check.names = FALSE)
  expect_false(anyNA(res$FDR))
  expect_false(anyNA(res$logFC))
})

test_that("cleaned-table writer keeps all columns for coefficient-style contrasts", {
  quiet_log()
  out.dir <- tempfile("dw_clean2_"); dir.create(out.dir)
  on.exit(unlink(out.dir, recursive = TRUE), add = TRUE)
  key <- data.frame(group = rep(c("control", "treated"), each = 4),
                    row.names = paste0("S", 1:8))
  d3 <- data.frame(ID = paste0("g", 1:5), gene_symbol = paste0("G", 1:5),
                   S1 = 1:5, S2 = 1:5, logFC = rnorm(5), FDR = runif(5, 0, 0.001),
                   stringsAsFactors = FALSE)
  # "groupstreated" cannot be mapped to the two groups -> must NOT drop sample columns
  diffr_expr_generate_cleaned_de_table_output(
    contrast = "groupstreated", annotated.normcnt = d3,
    samp.name.and.group.key = key, out.dir = out.dir,
    analysis.name = "t", filtered.lists = FALSE)

  f <- list.files(out.dir, pattern = "clean\\.tsv$", full.names = TRUE)
  res <- read.delim(f, check.names = FALSE)
  expect_true(all(c("S1", "S2") %in% names(res)))
})

# --- volcano helper --------------------------------------------------------

test_that("prepare_volcano_of_given_property returns a ggplot, incl. the no-hit case", {
  quiet_log()
  set.seed(1)
  d3 <- data.frame(gene_symbol = paste0("G", 1:200),
                   logFC = rnorm(200, 0, 2), PValue = runif(200))
  g <- prepare_volcano_of_given_property(d3, property.to.plot = "p",
                                         property.column = "PValue",
                                         property.thr = 0.05, logfc.thr = 1, numlab = 10)
  expect_s3_class(g, "ggplot")

  # nothing significant -> no labels, but still a valid plot (no 1:0 / empty-subset error)
  d3b <- data.frame(gene_symbol = paste0("G", 1:20),
                    logFC = rep(0, 20), PValue = rep(0.9, 20))
  expect_s3_class(
    prepare_volcano_of_given_property(d3b, "p", "PValue", 0.05, 1, numlab = 10),
    "ggplot")
})

# --- format_ensembl for mouse IDs ------------------------------------------

test_that("format_ensembl_ids_annotated_to_term detects mouse ENSMUSG columns", {
  quiet_log()
  # pre-fix, the column was found by grepl('ENSG', ...), which does NOT match ENSMUSG,
  # so this errored for mouse. The value conversion itself uses the offline convertId2().
  result <- data.frame(
    term   = c("GO:0001", "GO:0002"),
    geneID = c("ENSMUSG00000017146,ENSMUSG00000041147", "ENSMUSG00000059552"),
    stringsAsFactors = FALSE)
  out <- tryCatch(
    format_ensembl_ids_annotated_to_term(result, species = "Mouse"),
    error = function(e) skip(paste("convertId2 unavailable:", conditionMessage(e))))
  expect_s3_class(out, "data.frame")
  expect_equal(nrow(out), 2L)
})

# --- extract_contrasts plotting + heatmap path -----------------------------

test_that("diff_expr_extract_contrasts runs the plotting and heatmap path", {
  # exercises the heatmap branch (the 'id.col' fix and the hm.* thresholds) and the
  # per-contrast output directory handling, which had no direct coverage before
  skip_on_cran()
  skip_if_not_installed("pheatmap")
  quiet_log()

  ex        <- ex_dge()
  design    <- diff_expr_make_design(ex$samp.info, ex$groups)
  contrasts <- diff_expr_make_contrasts(design, ex$groups)
  fit       <- diff_expr_fit(ex$counts, ex$d, design, do.voom = FALSE, quasi.likelihood = TRUE)
  normcnt   <- edgeR::cpm(fit$d2, log = TRUE)

  out.dir <- tempfile("dw_ec_"); dir.create(out.dir)
  on.exit(unlink(out.dir, recursive = TRUE), add = TRUE)

  out <- suppressWarnings(diff_expr_extract_contrasts(
    contrasts = contrasts, fit = fit$fit, normcnt = normcnt, out.l = list(),
    do.voom = FALSE, out.dir = out.dir, analysis.name = "demo",
    plots = TRUE, lists = TRUE, heatmap.topn = 20,
    samp.info = ex$samp.info, samples = "SampleNames", groups = ex$groups))

  expect_true("treated-control" %in% names(out$contrasts))
  expect_true(dir.exists(file.path(out.dir, "treated-control")))       # per-contrast dir
  expect_false(is.null(out$heatmapPlots[["treated-control"]]))         # heatmap path executed
})

# --- diffr_venn default join.vec -------------------------------------------

test_that("diffr_venn runs with its default join.vec on minimal (symbol-only) tables", {
  quiet_log()
  skip_if_not_installed("venn")
  # the default is "gene_symbol", so tables carrying only that column must work
  mk <- function(sig) data.frame(gene_symbol = paste0("G", sig), fdr = 0.01,
                                  stringsAsFactors = FALSE)
  tabs <- list(A = mk(1:20), B = mk(10:30))
  v <- suppressWarnings(diffr_venn(tabs))          # no join.vec -> uses the default
  expect_type(v, "list")
  expect_true(all(c("venn.diagram", "venn.sections") %in% names(v)))
})

# --- fmt_dur: no rounding carry between the fields -------------------------

test_that("fmt_dur never prints a 60 in a subordinate field", {
  # pre-fix the seconds field was rounded independently of the minutes/hours field, so a
  # duration just below a boundary produced "1 min 60 sec" / "59 min 60 sec"
  expect_equal(fmt_dur(119.6),  "2 min 00 sec")
  expect_equal(fmt_dur(3599.6), "1 h 00 min 00 sec")
  expect_equal(fmt_dur(90),     "1 min 30 sec")
  expect_equal(fmt_dur(3600),   "1 h 00 min 00 sec")
  expect_equal(fmt_dur(45.2),   "45.2 sec")        # sub-minute keeps one decimal
  expect_false(grepl("60 sec", fmt_dur(119.6)))
  # non-finite / negative input is not formatted as a bogus duration
  expect_true(is.na(fmt_dur(NA_real_)))
  expect_true(is.na(fmt_dur(-1)))
})

# --- dw_dev_off: closing a device that is already gone ---------------------

test_that("dw_dev_off closes only open devices and never errors on the null device", {
  # a bare dev.off() with nothing but the null device left errors with
  # "cannot shut down device 1 (the null device)"; this wrapper must be a silent no-op
  expect_false(dw_dev_off(1L))                     # the null device is never in dev.list()

  f <- tempfile(fileext = ".pdf")
  on.exit(unlink(f), add = TRUE)
  grDevices::pdf(f)
  d <- grDevices::dev.cur()
  expect_true(dw_dev_off(d))                       # our device: closed
  expect_false(dw_dev_off(d))                      # already closed: no-op, no error
})

# --- diffExpr(): no '...' swallowing mistyped/legacy argument names --------

test_that("diffExpr rejects unknown arguments instead of silently ignoring them", {
  # 'biomart' is the internal name; the public argument is 'biom.use'. While diffExpr() had a
  # '...', such a call was silently swallowed and biomart annotation never ran.
  expect_error(diffExpr(biomart = TRUE), "unused argument")
  expect_error(diffExpr(heatmap.topn = 50), "unused argument")
})

# --- plot collections survive a second extract_contrasts call --------------

test_that("diff_expr_extract_contrasts extends, and never resets, the plot collections", {
  # diffExpr() calls this twice for designs whose remaining contrasts are fitted separately;
  # both calls share one 'out.l'. MAplots/volcanoPlots used to be reset on entry, so the first
  # call's plots were discarded (heatmapPlots, added later, was not - hence the inconsistency).
  skip_on_cran()
  skip_if_not_installed("pheatmap")
  quiet_log()

  ex        <- ex_dge()
  design    <- diff_expr_make_design(ex$samp.info, ex$groups)
  contrasts <- diff_expr_make_contrasts(design, ex$groups)
  fit       <- diff_expr_fit(ex$counts, ex$d, design, do.voom = FALSE, quasi.likelihood = TRUE)
  normcnt   <- edgeR::cpm(fit$d2, log = TRUE)

  out.dir <- tempfile("dw_acc_"); dir.create(out.dir)
  on.exit(unlink(out.dir, recursive = TRUE), add = TRUE)

  # stand in for the plots of an earlier call
  seeded <- list(contrasts = list(),
                 MAplots      = list(earlier = "keep"),
                 volcanoPlots = list(earlier = "keep"),
                 heatmapPlots = list(earlier = "keep"))

  out <- suppressWarnings(diff_expr_extract_contrasts(
    contrasts = contrasts, fit = fit$fit, normcnt = normcnt, out.l = seeded,
    do.voom = FALSE, out.dir = out.dir, analysis.name = "demo",
    plots = TRUE, lists = FALSE, heatmap.topn = 20,
    samp.info = ex$samp.info, samples = "SampleNames", groups = ex$groups))

  for (el in c("MAplots", "volcanoPlots", "heatmapPlots")) {
    expect_true("earlier" %in% names(out[[el]]))              # first call's plots survive
    expect_true("treated-control" %in% names(out[[el]]))      # this call's plots added
  }
})

test_that("no plot collections are created when plots = FALSE", {
  skip_on_cran()
  quiet_log()

  ex        <- ex_dge()
  design    <- diff_expr_make_design(ex$samp.info, ex$groups)
  contrasts <- diff_expr_make_contrasts(design, ex$groups)
  fit       <- diff_expr_fit(ex$counts, ex$d, design, do.voom = FALSE, quasi.likelihood = TRUE)
  normcnt   <- edgeR::cpm(fit$d2, log = TRUE)

  out.dir <- tempfile("dw_np_"); dir.create(out.dir)
  on.exit(unlink(out.dir, recursive = TRUE), add = TRUE)

  out <- suppressWarnings(diff_expr_extract_contrasts(
    contrasts = contrasts, fit = fit$fit, normcnt = normcnt, out.l = list(),
    do.voom = FALSE, out.dir = out.dir, analysis.name = "demo",
    plots = FALSE, lists = FALSE,
    samp.info = ex$samp.info, samples = "SampleNames", groups = ex$groups))

  expect_true("contrasts" %in% names(out))
  expect_false(any(c("MAplots", "volcanoPlots", "heatmapPlots") %in% names(out)))
})

# --- base.size reaches the plot theme --------------------------------------

test_that("base.size scales the volcano plot text", {
  quiet_log()
  set.seed(1)
  d3 <- data.frame(gene_symbol = paste0("G", 1:100), logFC = rnorm(100, 0, 2),
                   PValue = runif(100), FDR = runif(100), stringsAsFactors = FALSE)

  # diff_expr_volcano_plot() prints its plots, so send them to a scratch device
  scratch <- tempfile(fileext = ".pdf")
  grDevices::pdf(scratch)
  g <- tryCatch(diff_expr_volcano_plot(d3, id = "gene_symbol", sym.col = "gene_symbol",
                                       base.size = 22),
                finally = { dw_dev_off(); unlink(scratch) })

  expect_equal(g$FDR$theme$text$size, 22)
  expect_equal(g$Pval$theme$text$size, 22)
})
