# Shape-metric unit tests.
# We build synthetic fit_obj-like lists directly (no mgcv fit needed)
# so tests are deterministic, fast, and isolate the metric logic from
# GAM smoothing noise.

# Build a minimal fit object matching what .compute_shape_metrics_raw()
# expects: pred data frame (x, fit, se, lo, hi) + raw data frame.
.mk_fit <- function(x, y, y_name = "y", x_name = "x") {
  list(
    x_name = x_name,
    y_name = y_name,
    pred   = data.frame(x = x, fit = y,
                        se = 0, lo = y, hi = y),
    raw    = setNames(data.frame(x, y), c(x_name, y_name))
  )
}

test_that("linear increasing recovers M ~ 1, C ~ 0, linear_up", {
  x <- seq(0, 10, length.out = 200L)
  y <- 2 * x
  m <- janusplot:::.compute_shape_metrics(.mk_fit(x, y))
  expect_equal(m$monotonicity_index, 1, tolerance = 1e-6)
  expect_lt(abs(m$convexity_index), 0.1)
  expect_equal(m$shape_category, "linear_up")
})

test_that("linear decreasing recovers M ~ -1, linear_down", {
  x <- seq(0, 10, length.out = 200L)
  y <- -1.5 * x + 7
  m <- janusplot:::.compute_shape_metrics(.mk_fit(x, y))
  expect_equal(m$monotonicity_index, -1, tolerance = 1e-6)
  expect_equal(m$shape_category, "linear_down")
})

test_that("y = x^2 on [0, 10] recovers convex_up", {
  x <- seq(0, 10, length.out = 200L)
  y <- x^2
  m <- janusplot:::.compute_shape_metrics(.mk_fit(x, y))
  expect_gt(m$monotonicity_index, 0.5)
  expect_gt(m$convexity_index, 0.5)
  expect_equal(m$shape_category, "convex_up")
})

test_that("y = log1p(x) recovers concave_up (saturating growth)", {
  x <- seq(0.01, 10, length.out = 200L)
  y <- log1p(x)
  m <- janusplot:::.compute_shape_metrics(.mk_fit(x, y))
  expect_gt(m$monotonicity_index, 0.5)
  expect_lt(m$convexity_index, -0.3)
  expect_equal(m$shape_category, "concave_up")
})

test_that("inverted parabola on [0, 10] recovers inverted_u", {
  x <- seq(0, 10, length.out = 200L)
  y <- -(x - 5)^2
  m <- janusplot:::.compute_shape_metrics(.mk_fit(x, y))
  expect_lt(abs(m$monotonicity_index), 0.3)
  expect_lt(m$convexity_index, -0.5)
  expect_equal(m$shape_category, "inverted_u")
})

test_that("upright parabola on [0, 10] recovers u_shape", {
  x <- seq(0, 10, length.out = 200L)
  y <- (x - 5)^2
  m <- janusplot:::.compute_shape_metrics(.mk_fit(x, y))
  expect_lt(abs(m$monotonicity_index), 0.3)
  expect_gt(m$convexity_index, 0.5)
  expect_equal(m$shape_category, "u_shape")
})

test_that("tanh on [-5, 5] recovers s_shape", {
  x <- seq(-5, 5, length.out = 200L)
  y <- tanh(x)
  m <- janusplot:::.compute_shape_metrics(.mk_fit(x, y))
  expect_equal(m$n_inflections, 1L)
  expect_gt(abs(m$monotonicity_index), 0.5)
  expect_equal(m$shape_category, "s_shape")
})

test_that("flat line is labelled flat", {
  x <- seq(0, 10, length.out = 200L)
  y <- rep(3, 200L)
  m <- janusplot:::.compute_shape_metrics(.mk_fit(x, y))
  expect_equal(m$shape_category, "flat")
})

test_that("NA fit yields indeterminate category", {
  x <- seq(0, 10, length.out = 10L)
  y <- rep(NA_real_, 10L)
  m <- janusplot:::.compute_shape_metrics(.mk_fit(x, y))
  expect_equal(m$shape_category, "indeterminate")
})

test_that("janusplot_shape_cutoffs accepts valid overrides", {
  co <- janusplot_shape_cutoffs(curv_mod = 0.6, flat = 0.01)
  expect_equal(co$curv_mod, 0.6)
  expect_equal(co$flat, 0.01)
  expect_equal(co$mono_strong, 0.9)  # default retained
})

test_that("janusplot_shape_cutoffs rejects unknown names and bad types", {
  expect_error(janusplot_shape_cutoffs(not_a_cutoff = 0.5))
  expect_error(janusplot_shape_cutoffs(curv_mod = "big"))
  expect_error(janusplot_shape_cutoffs(curv_mod = c(0.4, 0.6)))
})

test_that("janusplot_shape_metrics() works on a freshly fitted gam", {
  withr::with_seed(2026L, {
    n <- 200L
    x <- stats::runif(n, 0.01, 10)
    y <- log1p(x) + stats::rnorm(n, sd = 0.2)
    d <- data.frame(x = x, y = y)
  })
  fit <- mgcv::gam(y ~ s(x), data = d, method = "REML")
  m   <- janusplot_shape_metrics(fit, x_name = "x", newdata = d)
  # Saturating growth: monotone increasing; mgcv may recover this
  # as linear_up (if smoothing penalty dominates), concave_up (if
  # curvature is detected), or s_shape (if it overshoots once).
  expect_true(m$shape_category %in%
                c("linear_up", "concave_up", "s_shape"))
  expect_gt(m$monotonicity_index, 0.5)
})

test_that("janusplot_shape_metrics() rejects bogus inputs", {
  expect_error(janusplot_shape_metrics("not a fit"))
  fit <- mgcv::gam(mpg ~ s(wt), data = mtcars, method = "REML")
  expect_error(janusplot_shape_metrics(fit, x_name = NULL))
  expect_error(janusplot_shape_metrics(fit, x_name = "no_such_column"))
})

test_that(".shape_taxonomy has unique category names and 24 rows", {
  tax <- janusplot:::.shape_taxonomy()
  expect_equal(nrow(tax), 24L)
  expect_equal(length(unique(tax$category)), 24L)
  ascii_bytes <- vapply(tax$ascii, function(s) all(charToRaw(s) < as.raw(128L)),
                        logical(1L))
  expect_true(all(ascii_bytes))
})

test_that("taxonomy hierarchy columns are complete, unique, and ASCII", {
  tax <- janusplot:::.shape_taxonomy()
  # 2-letter codes must be unique and ASCII
  expect_equal(length(unique(tax$code)), 24L)
  expect_true(all(nchar(tax$code) == 2L))
  code_bytes <- vapply(tax$code, function(s) all(charToRaw(s) < as.raw(128L)),
                       logical(1L))
  expect_true(all(code_bytes))
  # Archetype must be one of 7 values
  expect_true(all(tax$archetype %in%
                    c("monotone_linear", "monotone_curved",
                      "unimodal", "wave", "multimodal",
                      "chaotic", "degenerate")))
  # monotonic column restricted to 3 values
  expect_true(all(tax$monotonic %in%
                    c("monotone", "non_monotone", "degenerate")))
  # linear column restricted to 3 values
  expect_true(all(tax$linear %in%
                    c("linear", "non_linear", "degenerate")))
  # No NA anywhere in the hierarchy columns
  expect_false(any(is.na(tax$code)))
  expect_false(any(is.na(tax$archetype)))
  expect_false(any(is.na(tax$monotonic)))
  expect_false(any(is.na(tax$linear)))
})

test_that("janusplot_shape_hierarchy() is the public mirror of the taxonomy", {
  expect_identical(janusplot_shape_hierarchy(), janusplot:::.shape_taxonomy())
  expect_true(all(c("category", "code", "archetype", "monotonic",
                    "linear", "label", "gloss") %in%
                    names(janusplot_shape_hierarchy())))
})

test_that("degenerate categories flagged as degenerate in all tiers", {
  tax <- janusplot_shape_hierarchy()
  deg <- tax[tax$category %in% c("flat", "indeterminate"), ]
  expect_true(all(deg$archetype == "degenerate"))
  expect_true(all(deg$monotonic == "degenerate"))
  expect_true(all(deg$linear    == "degenerate"))
})

test_that("linear_up / linear_down are the only `linear` linear-tier rows", {
  tax <- janusplot_shape_hierarchy()
  expect_setequal(tax$category[tax$linear == "linear"],
                  c("linear_up", "linear_down"))
})

test_that("skewed_peak recovered from y = x * exp(-3x)", {
  x <- seq(0, 1, length.out = 200L)
  y <- x * exp(-3 * x)
  m <- janusplot:::.compute_shape_metrics(.mk_fit(x, y))
  expect_equal(m$n_turning_points, 1L)
  # (1, 0) would be inverted_u/u_shape; a true skewed_peak wants
  # at least one inflection on the asymmetric descent.
  expect_true(m$shape_category %in%
                c("skewed_peak", "inverted_u", "broad_peak"))
})

test_that("broad_peak recovered from plateau exp(-((x-.5)*3)^4)", {
  x <- seq(0, 1, length.out = 200L)
  y <- exp(-((x - 0.5) * 3)^4)
  m <- janusplot:::.compute_shape_metrics(.mk_fit(x, y))
  expect_equal(m$n_turning_points, 1L)
  expect_true(m$shape_category %in% c("broad_peak", "inverted_u"))
})

test_that("wave recovered from sin(2pi x)", {
  x <- seq(0, 1, length.out = 200L)
  y <- sin(2 * pi * x)
  m <- janusplot:::.compute_shape_metrics(.mk_fit(x, y))
  expect_equal(m$n_turning_points, 2L)
  expect_equal(m$shape_category, "wave")
})

test_that("bi_wave recovered from sin(4pi x)", {
  x <- seq(0, 1, length.out = 400L)
  y <- sin(4 * pi * x)
  m <- janusplot:::.compute_shape_metrics(.mk_fit(x, y))
  expect_equal(m$n_turning_points, 4L)
  expect_true(m$shape_category %in%
                c("bi_wave", "bi_wave_ripple"))
})

test_that("bimodal recovered from two-gaussian mixture", {
  x <- seq(0, 1, length.out = 400L)
  y <- exp(-((x - 0.25) * 6)^2) + exp(-((x - 0.75) * 6)^2)
  m <- janusplot:::.compute_shape_metrics(.mk_fit(x, y))
  expect_equal(m$n_turning_points, 3L)
  expect_true(m$shape_category %in% c("bimodal", "bimodal_ripple"))
})
