# =============================================================================
# test-job.R — Unit tests for job_ series (mock-based, no network required)
# =============================================================================
.skip_if_no_mockery()

# Helper: build a fake .dx_run() result
.fake_dx <- function(stdout = "", stderr = "", status = 0) {
  list(stdout = stdout, stderr = stderr, status = status, success = status == 0)
}

# Fake describe list — done job
.fake_desc_done <- function() {
  list(
    state          = "done",
    runInput       = list(output = "ad_pheno"),
    output         = list(csv = list(list(`$dnanexus_link` = "file-XXXX"))),
    failureMessage = NULL,
    failureReason  = NULL
  )
}

# Fake describe list — failed job
.fake_desc_failed <- function() {
  list(
    state          = "failed",
    runInput       = list(output = "ad_pheno"),
    output         = NULL,
    failureMessage = "AppError: invalid field names",
    failureReason  = "AppError"
  )
}

# Fake describe list — running job
.fake_desc_running <- function() {
  list(
    state          = "running",
    runInput       = list(output = "ad_pheno"),
    output         = list(),
    failureMessage = NULL,
    failureReason  = NULL
  )
}

# Fake dx find jobs stdout (3 real jobs + trailing "More results" line)
.fake_find_jobs_stdout <- function() {
  paste(
    "* Table exporter (table-exporter:main) (done) job-AAAAAAAAAAAA",
    "  user001 2026-03-03 17:15:21 (runtime 0:04:36)",
    "* Table exporter (table-exporter:main) (failed) job-BBBBBBBBBBBB",
    "  user001 2026-03-03 16:57:07 (runtime 0:03:25)",
    "* Table exporter (table-exporter:main) (running) job-CCCCCCCCCCCC",
    "  user001 2026-03-03 18:27:02",
    "* More results not shown; use -n to increase number of results or",
    "  --created-before to show older results",
    sep = "\n"
  )
}

# ===========================================================================
# .dx_job_output_id() — pure function, no mocking needed
# ===========================================================================

test_that(".dx_job_output_id() returns file ID from done describe", {
  id <- ukbflow:::.dx_job_output_id(.fake_desc_done())
  expect_equal(id, "file-XXXX")
})

test_that(".dx_job_output_id() stops when output is NULL (failed job)", {
  expect_error(
    ukbflow:::.dx_job_output_id(.fake_desc_failed()),
    "no output CSV"
  )
})

test_that(".dx_job_output_id() stops when output$csv is empty list", {
  desc <- .fake_desc_done()
  desc$output$csv <- list()
  expect_error(ukbflow:::.dx_job_output_id(desc), "no output CSV")
})

# ===========================================================================
# .dx_parse_jobs() — pure function, no mocking needed
# ===========================================================================

test_that(".dx_parse_jobs() returns empty data.frame for empty stdout", {
  result <- ukbflow:::.dx_parse_jobs("")
  expect_s3_class(result, "data.frame")
  expect_equal(nrow(result), 0)
  expect_named(result, c("job_id", "name", "state", "created", "runtime"))
})

test_that(".dx_parse_jobs() parses done/failed/running jobs correctly", {
  result <- ukbflow:::.dx_parse_jobs(.fake_find_jobs_stdout())
  expect_equal(nrow(result), 3)
  expect_equal(result$job_id,  c("job-AAAAAAAAAAAA", "job-BBBBBBBBBBBB", "job-CCCCCCCCCCCC"))
  expect_equal(result$state,   c("done", "failed", "running"))
})

test_that(".dx_parse_jobs() skips 'More results not shown' line", {
  result <- ukbflow:::.dx_parse_jobs(.fake_find_jobs_stdout())
  expect_equal(nrow(result), 3)
  expect_false(any(grepl("More results", result$job_id)))
})

test_that(".dx_parse_jobs() sets NA runtime for running job", {
  result <- ukbflow:::.dx_parse_jobs(.fake_find_jobs_stdout())
  expect_true(is.na(result$runtime[result$state == "running"]))
  expect_equal(result$runtime[result$state == "done"], "0:04:36")
})

test_that(".dx_parse_jobs() returns POSIXct created column", {
  result <- ukbflow:::.dx_parse_jobs(.fake_find_jobs_stdout())
  expect_s3_class(result$created, "POSIXct")
})

test_that(".dx_parse_jobs() returns correct column names", {
  result <- ukbflow:::.dx_parse_jobs(.fake_find_jobs_stdout())
  expect_named(result, c("job_id", "name", "state", "created", "runtime"))
})

# ===========================================================================
# job_status()
# ===========================================================================

test_that("job_status() returns named character for done job", {
  mockery::stub(job_status, ".dx_job_describe", function(...) .fake_desc_done())
  result <- job_status("job-XXXX")
  expect_type(result, "character")
  expect_equal(result[[1]], "done")
  expect_equal(names(result), "job-XXXX")
})

test_that("job_status() returns 'failed' with failure_message attr", {
  mockery::stub(job_status, ".dx_job_describe", function(...) .fake_desc_failed())
  result <- job_status("job-XXXX")
  expect_equal(result[[1]], "failed")
  expect_equal(attr(result, "failure_message"), "AppError: invalid field names")
})

test_that("job_status() failure_message falls back to failureReason", {
  desc <- .fake_desc_failed()
  desc$failureMessage <- NULL
  mockery::stub(job_status, ".dx_job_describe", function(...) desc)
  result <- job_status("job-XXXX")
  expect_equal(attr(result, "failure_message"), "AppError")
})

test_that("job_status() returns NULL failure_message for done job", {
  mockery::stub(job_status, ".dx_job_describe", function(...) .fake_desc_done())
  result <- job_status("job-XXXX")
  expect_null(attr(result, "failure_message"))
})

test_that("job_status() stops on invalid job_id format", {
  expect_error(job_status("notajob"), "job-XXXX")
})

test_that("job_status() stops on NA job_id", {
  expect_error(job_status(NA_character_))
})

test_that("job_status() stops on length > 1 job_id", {
  expect_error(job_status(c("job-AAA", "job-BBB")))
})

# ===========================================================================
# job_wait()
# ===========================================================================

test_that("job_wait() returns invisibly when job is already done", {
  mockery::stub(job_wait, ".dx_job_describe", function(...) .fake_desc_done())
  expect_invisible(suppressMessages(job_wait("job-XXXX", verbose = FALSE)))
  result <- suppressMessages(job_wait("job-XXXX", verbose = FALSE))
  expect_equal(result, "done")
})

test_that("job_wait() stops with message when job failed", {
  mockery::stub(job_wait, ".dx_job_describe", function(...) .fake_desc_failed())
  expect_error(
    suppressMessages(job_wait("job-XXXX", verbose = FALSE)),
    "AppError: invalid field names"
  )
})

test_that("job_wait() stops when job is terminated", {
  desc_terminated <- .fake_desc_done()
  desc_terminated$state <- "terminated"
  mockery::stub(job_wait, ".dx_job_describe", function(...) desc_terminated)
  expect_error(
    suppressMessages(job_wait("job-XXXX", verbose = FALSE)),
    "terminated"
  )
})

test_that("job_wait() stops on timeout", {
  mockery::stub(job_wait, ".dx_job_describe", function(...) .fake_desc_running())
  expect_error(
    suppressMessages(job_wait("job-XXXX", timeout = 0, verbose = FALSE)),
    "Timed out"
  )
})

test_that("job_wait() stops on invalid job_id format", {
  expect_error(job_wait("notajob"), "job-XXXX")
})


# ===========================================================================
# job_path()
# ===========================================================================

test_that("job_path() stops on invalid job_id format", {
  expect_error(job_path("notajob"), "job-XXXX")
})

test_that("job_path() stops when job is not done", {
  mockery::stub(job_path, ".dx_job_describe", function(...) .fake_desc_running())
  expect_error(suppressMessages(job_path("job-XXXX")), "not 'done'")
})

test_that("job_path() stops when job failed", {
  mockery::stub(job_path, ".dx_job_describe", function(...) .fake_desc_failed())
  expect_error(suppressMessages(job_path("job-XXXX")), "not 'done'")
})

test_that("job_path() returns /mnt/project/ path for done job", {
  mockery::stub(job_path, ".dx_job_describe", function(...) .fake_desc_done())
  mockery::stub(job_path, ".dx_file_path",
                function(...) "/mnt/project/ad_pheno.csv")
  result <- job_path("job-XXXX")
  expect_equal(result, "/mnt/project/ad_pheno.csv")
})

# ===========================================================================
# job_result()
# ===========================================================================

test_that("job_result() stops when not on RAP", {
  mockery::stub(job_result, ".assert_on_rap", function() cli::cli_abort("This function must be run inside the RAP environment."))
  expect_error(job_result("job-XXXX"), "RAP environment")
})

test_that("job_result() stops on invalid job_id format", {
  mockery::stub(job_result, ".assert_on_rap", function() invisible(NULL))
  expect_error(job_result("notajob"), "job-XXXX")
})

test_that("job_result() stops when job is not done", {
  mockery::stub(job_result, ".assert_on_rap", function() invisible(NULL))
  mockery::stub(job_result, "job_path", function(...) {
    stop("Job job-XXXX is 'running', not 'done'.")
  })
  expect_error(suppressMessages(job_result("job-XXXX")), "not 'done'")
})

test_that("job_result() returns a data.table when on RAP", {
  tmp <- tempfile(fileext = ".csv")
  write.csv(data.frame(eid = c(1L, 2L), p31 = c(0L, 1L)), tmp, row.names = FALSE)

  mockery::stub(job_result, ".assert_on_rap", function() invisible(NULL))
  mockery::stub(job_result, "job_path", function(...) tmp)

  result <- suppressMessages(job_result("job-XXXX"))
  expect_true(data.table::is.data.table(result))
  expect_equal(nrow(result), 2L)
})

# ===========================================================================
# job_ls()
# ===========================================================================

test_that("job_ls() returns a data.frame with correct columns", {
  mockery::stub(job_ls, ".dx_find_jobs_raw",
                function(...) .fake_dx(stdout = .fake_find_jobs_stdout()))
  result <- job_ls()
  expect_s3_class(result, "data.frame")
  expect_named(result, c("job_id", "name", "state", "created", "runtime"))
})

test_that("job_ls() returns all jobs when state = NULL", {
  mockery::stub(job_ls, ".dx_find_jobs_raw",
                function(...) .fake_dx(stdout = .fake_find_jobs_stdout()))
  result <- job_ls()
  expect_equal(nrow(result), 3L)
})

test_that("job_ls() filters by state correctly", {
  mockery::stub(job_ls, ".dx_find_jobs_raw",
                function(...) .fake_dx(stdout = .fake_find_jobs_stdout()))
  result <- job_ls(state = "done")
  expect_equal(nrow(result), 1L)
  expect_true(all(result$state == "done"))
})

test_that("job_ls() filters by multiple states", {
  mockery::stub(job_ls, ".dx_find_jobs_raw",
                function(...) .fake_dx(stdout = .fake_find_jobs_stdout()))
  result <- job_ls(state = c("done", "failed"))
  expect_equal(nrow(result), 2L)
  expect_true(all(result$state %in% c("done", "failed")))
})

test_that("job_ls() returns empty data.frame when no jobs match state filter", {
  mockery::stub(job_ls, ".dx_find_jobs_raw",
                function(...) .fake_dx(stdout = .fake_find_jobs_stdout()))
  result <- job_ls(state = "terminated")
  expect_equal(nrow(result), 0L)
})

test_that("job_ls() stops when dx find jobs fails", {
  mockery::stub(job_ls, ".dx_find_jobs_raw",
                function(...) .fake_dx(stderr = "Not logged in", status = 1))
  expect_error(job_ls(), "Failed to list jobs")
})

# ---------------------------------------------------------------------------
# job_ls() — n validation
# ---------------------------------------------------------------------------

test_that("job_ls() stops when n is character", {
  expect_error(job_ls(n = "five"), "positive integer")
})

test_that("job_ls() stops when n is 0", {
  expect_error(job_ls(n = 0), "positive integer")
})

test_that("job_ls() stops when n is negative", {
  expect_error(job_ls(n = -1), "positive integer")
})

test_that("job_ls() stops when n is Inf", {
  expect_error(job_ls(n = Inf), "positive integer")
})

test_that("job_ls() stops when n is non-whole number", {
  expect_error(job_ls(n = 1.5), "positive integer")
})

# ---------------------------------------------------------------------------
# job_ls() — state validation
# ---------------------------------------------------------------------------

test_that("job_ls() stops when state is non-character", {
  expect_error(job_ls(state = TRUE), "character vector")
})

test_that("job_ls() stops when state contains an invalid value", {
  expect_error(job_ls(state = "oops"), "oops")
})

test_that("job_ls() stops when state contains mix of valid and invalid values", {
  expect_error(job_ls(state = c("done", "oops")), "oops")
})
