# Tests for GRM C++ Implementation

test_that("GRM helper functions work correctly", {
  # Test target_to_params_grm
  test_target <- c(0, 0.5, -1, 0, log(0.5), 0)
  test_nitems <- 2
  test_ncat <- c(3, 3)

  params_result <- target_to_params_grm(test_target, test_nitems, test_ncat)

  expect_type(params_result, "list")
  expect_named(params_result, c("a", "b"))
  expect_length(params_result$a, 2)
  expect_length(params_result$b, 2)
  expect_true(all(params_result$a > 0)) # Discrimination should be positive

  # Test simple_quadrature_grm
  quad_result <- simple_quadrature_grm(21)

  expect_type(quad_result, "list")
  expect_named(quad_result, c("nodes", "weights"))
  expect_length(quad_result$nodes, 21)
  expect_length(quad_result$weights, 21)
  expect_equal(sum(quad_result$weights), 1, tolerance = 1e-10)
})

test_that("log_lik_grm_cpp works correctly", {
  # Create small test dataset
  set.seed(123)
  small_data <- matrix(sample(1:3, 30, replace = TRUE), nrow = 10, ncol = 3)

  # Generate reasonable starting values
  start_vals <- generate_start_values(small_data)
  target <- params_to_target(start_vals$a, start_vals$b)

  ll_result <- log_lik_grm_cpp(target, small_data, 21)

  expect_type(ll_result, "double")
  expect_true(is.finite(ll_result))
  expect_true(ll_result < 0) # Log-likelihood should be negative
})

test_that("score_function_analytical_grm works correctly", {
  # Create small test dataset
  set.seed(456)
  small_data <- matrix(sample(1:4, 40, replace = TRUE), nrow = 10, ncol = 4)

  # Generate starting values
  start_vals <- generate_start_values(small_data)
  target <- params_to_target(start_vals$a, start_vals$b)

  # Test analytical gradient
  grad_result <- score_function_analytical_grm(target, small_data, 21)

  expect_type(grad_result, "double")
  expect_length(grad_result, length(target))
  expect_true(all(is.finite(grad_result)))
})

test_that("compare_gradients_grm works correctly", {
  # Create small test dataset
  set.seed(789)
  small_data <- matrix(sample(1:3, 24, replace = TRUE), nrow = 8, ncol = 3)

  # Generate starting values
  start_vals <- generate_start_values(small_data)
  target <- params_to_target(start_vals$a, start_vals$b)

  # Compare gradients
  comparison <- compare_gradients_grm(target, small_data, 21)

  expect_type(comparison, "list")
  expect_named(comparison, c("analytical", "numerical", "difference", "max_abs_diff", "rms_diff"))
  expect_length(comparison$analytical, length(target))
  expect_length(comparison$numerical, length(target))
  expect_type(comparison$max_abs_diff, "double")
  expect_type(comparison$rms_diff, "double")

  # Gradients should be reasonably close
  expect_true(comparison$max_abs_diff < 1e-2)
})

test_that("GRM model fitting works correctly", {
  # Use example data and ensure ordinal type
  data("J5S1000", package = "exametrika")
  small_data <- J5S1000$Q[1:50, 1:3]

  # Format data explicitly as ordinal
  formatted_data <- dataFormat(small_data, response.type = "ordinal")

  # Fit GRM model
  grm_result <- GRM(formatted_data, verbose = FALSE)

  expect_s3_class(grm_result, c("exametrika", "GRM"))
  expect_named(grm_result, c(
    "testlength", "nobs", "log_lik", "iterations",
    "params", "EAP", "MAP", "PSD",
    "ItemFitIndices", "TestFitIndices"
  ))

  # Check dimensions
  expect_equal(grm_result$testlength, 3)
  expect_equal(grm_result$nobs, 50)
  expect_equal(nrow(grm_result$params), 3)
  expect_length(grm_result$EAP, 50)
  expect_length(grm_result$MAP, 50)
  expect_length(grm_result$PSD, 50)

  # Check parameter estimates are reasonable
  expect_true(all(grm_result$params$Slope > 0))
  expect_true(all(is.finite(grm_result$params$Slope)))
  expect_true(is.finite(grm_result$log_lik))
  expect_true(grm_result$iterations[[1]] >= 1)
})

test_that("GRM performance is acceptable", {
  # Create medium-sized dataset
  set.seed(999)
  medium_data <- matrix(sample(1:4, 200, replace = TRUE), nrow = 50, ncol = 4)

  # Format as ordinal explicitly
  formatted_medium <- dataFormat(medium_data, response.type = "ordinal")

  # Time the fitting
  start_time <- Sys.time()
  grm_result <- GRM(formatted_medium, verbose = FALSE)
  elapsed <- as.numeric(Sys.time() - start_time, units = "secs")

  expect_true(elapsed < 10) # Should complete within 10 seconds
  expect_s3_class(grm_result, "GRM")
  expect_true(is.finite(grm_result$log_lik))
})

test_that("GRM ItemFitIndices has correct structure and values", {
  # Use example data with mixed category counts
  data("J5S1000", package = "exametrika")
  small_data <- J5S1000$Q[1:50, 1:3]
  formatted_data <- dataFormat(small_data, response.type = "ordinal")
  grm_result <- GRM(formatted_data, verbose = FALSE)

  nitems <- grm_result$testlength

  # ItemFitIndices: each field should be a vector of length nitems
  ifi <- grm_result$ItemFitIndices
  expect_s3_class(ifi, "ModelFit")
  expect_length(ifi$model_log_like, nitems)
  expect_length(ifi$bench_log_like, nitems)
  expect_length(ifi$null_log_like, nitems)
  expect_length(ifi$model_Chi_sq, nitems)
  expect_length(ifi$null_Chi_sq, nitems)
  expect_length(ifi$model_df, nitems)
  expect_length(ifi$null_df, nitems)
  expect_true(all(is.finite(ifi$model_log_like)))
  expect_true(all(is.finite(ifi$null_log_like)))
  expect_true(all(is.finite(ifi$bench_log_like)))

  # Log-likelihoods should be negative (log of probabilities)
  expect_true(all(ifi$model_log_like < 0))
  expect_true(all(ifi$null_log_like < 0))
  expect_true(all(ifi$bench_log_like < 0))

  # Bench (saturated) >= Analysis >= Null in terms of log-likelihood
  expect_true(all(ifi$bench_log_like >= ifi$model_log_like - 1e-10))
  expect_true(all(ifi$model_log_like >= ifi$null_log_like - 1e-10))

  # Chi-square should be non-negative
  expect_true(all(ifi$model_Chi_sq >= -1e-10))
  expect_true(all(ifi$null_Chi_sq >= -1e-10))

  # TestFitIndices: each field should be a scalar
  tfi <- grm_result$TestFitIndices
  expect_s3_class(tfi, "ModelFit")
  expect_length(tfi$model_log_like, 1)
  expect_length(tfi$bench_log_like, 1)
  expect_length(tfi$null_log_like, 1)
  expect_true(is.finite(tfi$model_log_like))
  expect_true(is.finite(tfi$null_log_like))

  # TestFitIndices should be sums of ItemFitIndices
  expect_equal(tfi$model_log_like, sum(ifi$model_log_like), tolerance = 1e-10)
  expect_equal(tfi$bench_log_like, sum(ifi$bench_log_like), tolerance = 1e-10)
  expect_equal(tfi$null_log_like, sum(ifi$null_log_like), tolerance = 1e-10)
})

test_that("GRM works correctly with uniform category counts (apply/table bug regression)", {
  # This test specifically targets the apply()/table() simplification bug:
  # When all items have the same number of categories, apply(X, 2, table)
  # returns a matrix instead of a list, causing response_list[[j]] to fail.
  # The fix uses lapply(seq_len(nitems), function(j) table(X[, j])) instead.

  # Create data where ALL items have the same number of categories (5)
  set.seed(42)
  nobs <- 80
  nitems <- 4
  uniform_data <- matrix(sample(1:5, nobs * nitems, replace = TRUE),
    nrow = nobs, ncol = nitems
  )
  formatted_data <- dataFormat(uniform_data, response.type = "ordinal")

  # This should NOT error (the bug caused errors here)
  grm_result <- GRM(formatted_data, verbose = FALSE)

  expect_s3_class(grm_result, c("exametrika", "GRM"))
  expect_equal(grm_result$testlength, nitems)
  expect_equal(grm_result$nobs, nobs)

  # ItemFitIndices should have correct structure
  ifi <- grm_result$ItemFitIndices
  expect_length(ifi$model_log_like, nitems)
  expect_length(ifi$null_log_like, nitems)
  expect_true(all(is.finite(ifi$model_log_like)))
  expect_true(all(is.finite(ifi$null_log_like)))

  # null_log_like should be properly computed (not NaN or Inf)
  expect_true(all(ifi$null_log_like < 0))

  # TestFitIndices should be scalar sums
  tfi <- grm_result$TestFitIndices
  expect_equal(tfi$model_log_like, sum(ifi$model_log_like), tolerance = 1e-10)
  expect_equal(tfi$null_log_like, sum(ifi$null_log_like), tolerance = 1e-10)
})

test_that("GRM fit indices are finite for moderate item counts (exp(-nitems*100) underflow regression)", {
  skip_on_cran()
  # For nitems >= 8, const <- exp(-nitems * 100) underflowed to 0 and
  # 0 * log(0) poisoned every item/test fit index with NaN.
  set.seed(20260422)
  nitems <- 20
  nobs <- 250
  dat <- matrix(sample(1:3, nobs * nitems, replace = TRUE),
    nrow = nobs, ncol = nitems
  )
  fd <- dataFormat(dat, response.type = "ordinal")
  res <- GRM(fd, verbose = FALSE)

  ifi <- res$ItemFitIndices
  expect_true(all(is.finite(ifi$bench_log_like)))
  expect_true(all(is.finite(ifi$model_Chi_sq)))
  expect_true(all(is.finite(ifi$CFI)))
  expect_true(all(is.finite(ifi$RMSEA)))

  tfi <- res$TestFitIndices
  expect_true(is.finite(tfi$bench_log_like))
  expect_true(is.finite(tfi$RMSEA))
  expect_true(is.finite(tfi$AIC))
  expect_true(is.finite(tfi$BIC))
})

test_that("GRM accepts 0-based ordinal responses (ncat max-value regression)", {
  skip_on_cran()
  # ncat was derived from apply(dat, 2, max), so 0..K-1 coded data and
  # sparse codings (e.g. 1,2,4) silently dropped the top category.
  data("J15S3810", package = "exametrika")
  res <- GRM(J15S3810, verbose = FALSE)

  expect_s3_class(res, c("exametrika", "GRM"))
  expect_equal(res$testlength, 15)
  # J15S3810 items have 4 categories each, so 3 thresholds per item.
  expect_equal(ncol(res$params), 4L)
  expect_true(all(is.finite(res$params$Slope)))
  expect_true(all(is.finite(res$TestFitIndices$RMSEA)))

  # A gapped coding (categories 1, 2, 4) must estimate the same number
  # of thresholds as a contiguous 3-category coding.
  set.seed(1)
  gapped <- matrix(sample(c(1, 2, 4), 200 * 5, replace = TRUE),
    nrow = 200, ncol = 5
  )
  fd <- dataFormat(gapped, response.type = "ordinal")
  res_g <- GRM(fd, verbose = FALSE)
  expect_equal(ncol(res_g$params), 3L)
  expect_true(all(is.finite(res_g$TestFitIndices$AIC)))
})

test_that("GRM error handling works", {
  # Test with inappropriate data
  expect_error(GRM(matrix(letters[1:20], nrow = 5, ncol = 4)))

  # Test with insufficient data
  tiny_data <- matrix(c(1, 2, 1, 2), nrow = 2, ncol = 2)
  expect_error(GRM(tiny_data))
})
