# --- latex_grob creation and structure ---

test_that("latex_grob creates valid grob and returns correct dimensions", {
  pdf(NULL); on.exit(dev.off(), add = TRUE)
  g <- latex_grob("\\frac{x^{2}+1}{\\sqrt{y}}")
  expect_s3_class(g, "latexgrob")
  expect_true(nrow(g$layout_df) > 0)
  expect_true(g$bbox_w > 0)

  # render_mode stored correctly
  g_path <- latex_grob("x^2", render_mode = "path")
  g_type <- latex_grob("x^2", render_mode = "typeface")
  expect_equal(g_path$render_mode, "path")
  expect_null(g_path$path_layout_df)
  expect_s3_class(g_type$path_layout_df, "data.frame")

  # latex_dims with fontsize scaling
  dims <- latex_dims("\\frac{a}{b}", render_mode = "path")
  expect_true(grid::convertWidth(dims$width, "points", valueOnly = TRUE) > 0)
  w_small <- grid::convertWidth(latex_dims("x^2", gp = grid::gpar(fontsize = 10))$width,
                                "bigpts", valueOnly = TRUE)
  w_large <- grid::convertWidth(latex_dims("x^2", gp = grid::gpar(fontsize = 40))$width,
                                "bigpts", valueOnly = TRUE)
  expect_true(w_large > w_small)
})

# --- latex_grob parameters ---

test_that("latex_grob parameters work correctly", {
  pdf(NULL); on.exit(dev.off(), add = TRUE)
  # Rotation
  expect_equal(latex_grob("x^2", rot = 45)$vp$angle, 45)

  # max_width
  g_mw <- latex_grob("x^2 + y^2 = z^2", max_width = 50, render_mode = "path")
  expect_true(grid::convertWidth(grid::grobWidth(g_mw), "bigpts", valueOnly = TRUE) > 0)

  # makeContent builds children
  g_mc <- grid::makeContent(latex_grob("\\frac{a}{b}", render_mode = "path"))
  expect_true(length(g_mc$children) > 0)

  # width/height details positive
  g <- latex_grob("\\frac{a}{b}", render_mode = "path")
  expect_true(grid::convertWidth(grid::widthDetails(g), "bigpts", valueOnly = TRUE) > 0)
  expect_true(grid::convertHeight(grid::heightDetails(g), "bigpts", valueOnly = TRUE) > 0)
})

# --- device support and typeface rendering ---

test_that("device support detection and typeface fallback work", {
  # pdf reports glyphs=TRUE
  tf_pdf <- tempfile(fileext = ".pdf")
  grDevices::pdf(tf_pdf)
  expect_true(gridmicrotex:::.device_supports_typeface_glyphs())

  # PDF: renders without fallback warning
  expect_no_warning({
    g <- latex_grob("\\frac{a}{b}", render_mode = "typeface", gp = grid::gpar(fontsize = 20))
    grid::grid.newpage()
    grid::grid.draw(g)
  })
  grDevices::dev.off()
  unlink(tf_pdf)

  # Postscript: falls back with warning
  tf_ps <- tempfile(fileext = ".ps")
  grDevices::postscript(tf_ps)
  gridmicrotex:::.clear_typeface_noted()
  on.exit({ grDevices::dev.off(); unlink(tf_ps) }, add = TRUE)
  expect_false(gridmicrotex:::.device_supports_typeface_glyphs())
  expect_message({
    g <- latex_grob("\\frac{a}{b}", render_mode = "typeface",
                    gp = grid::gpar(fontsize = 20))
    grid::grid.newpage()
    grid::grid.draw(g)
  }, "falling back to path mode")
  # The fallback has to substitute the path layout, not merely warn: the
  # children it draws must be outlines, not glyphs the device cannot set.
  kids <- suppressMessages(grid::makeContent(g))$children
  expect_true("pathgrob" %in% vapply(kids, function(k) class(k)[1], ""))
})

# --- edge cases: empty and invalid input ---

test_that("latex_grob handles empty input as a zero-size grob", {
  g <- latex_grob("")
  expect_s3_class(g, "latexgrob")
  expect_equal(g$bbox_w, 0)
  expect_equal(g$bbox_h, 0)
  expect_equal(nrow(g$layout_df), 0L)
})

test_that("an unknown command is set as its own name, not dropped", {
  # MicroTeX is lenient rather than strict: \notavalidcommand comes out as
  # the letters of its name followed by its argument. Rendering nothing
  # would hide a typo completely, so assert the name is actually drawn.
  g <- latex_grob("\\notavalidcommand{x}", input_mode = "math")
  expect_gte(nrow(g$layout_df), nchar("notavalidcommand"))
  expect_gt(g$bbox_w, 0)
})

# --- editGrob: re-parse on parse-affecting fields ---

test_that("editGrob re-parses when tex changes", {
  g <- latex_grob("x", render_mode = "path")
  g2 <- grid::editGrob(g, tex = "x^{2} + y^{2} + z^{2}")
  expect_equal(g2$tex, "x^{2} + y^{2} + z^{2}")
  expect_true(g2$bbox_w > g$bbox_w)
  # That the layout is genuinely rebuilt, without assuming how many
  # records a given string produces -- consecutive text is drawn as one
  # run when nothing wraps, so record count is not a proxy for length.
  expect_false(identical(g2$layout_df, g$layout_df))
  # viewport width/height tracked bbox
  expect_equal(
    as.numeric(g2$vp$width),
    as.numeric(grid::unit(g2$bbox_w, "bigpts"))
  )
})

test_that("editGrob re-parses when gp (fontsize) changes", {
  g20 <- latex_grob("x^2", render_mode = "path", gp = grid::gpar(fontsize = 20))
  g40 <- grid::editGrob(g20, gp = grid::gpar(fontsize = 40))
  expect_equal(g40$fontsize, 40)
  # 2x font -> ~2x bbox
  expect_equal(g40$bbox_w / g20$bbox_w, 2, tolerance = 0.05)
  # gp is re-stripped after parse (fontsize not carried on the gTree)
  expect_null(g40$gp$fontsize)
})

test_that("editGrob re-parses when math_font / tex_style / render_mode change", {
  g <- latex_grob("\\frac{a}{b}", render_mode = "path", math_font = "lete")
  g2 <- grid::editGrob(g, math_font = "stix")
  expect_false(identical(g$layout_df, g2$layout_df))

  g3 <- latex_grob("\\sum_{i=1}^{n} i", render_mode = "path", tex_style = "text")
  g4 <- grid::editGrob(g3, tex_style = "display")
  expect_true(g4$bbox_h > g3$bbox_h)  # display makes \sum taller

  g5 <- latex_grob("x^2", render_mode = "path")
  g6 <- grid::editGrob(g5, render_mode = "typeface")
  expect_equal(g6$render_mode, "typeface")
  expect_s3_class(g6$path_layout_df, "data.frame")  # fallback layout generated
})

test_that("editGrob on non-parse fields does not re-parse", {
  g <- latex_grob("\\frac{a}{b}", render_mode = "path")
  orig_layout <- g$layout_df
  g2 <- grid::editGrob(g, debug = TRUE)
  expect_true(isTRUE(g2$debug))
  expect_identical(g2$layout_df, orig_layout)
})

test_that("ascentDetails + descentDetails sum to heightDetails", {
  pdf(NULL); on.exit(dev.off(), add = TRUE)
  g <- latex_grob("\\frac{a}{b}", render_mode = "path", gp = grid::gpar(fontsize = 24))
  asc  <- grid::convertHeight(grid::ascentDetails(g),  "bigpts", valueOnly = TRUE)
  desc <- grid::convertHeight(grid::descentDetails(g), "bigpts", valueOnly = TRUE)
  h    <- grid::convertHeight(grid::heightDetails(g),  "bigpts", valueOnly = TRUE)
  expect_equal(asc + desc, h, tolerance = 1e-6)
  expect_true(asc > 0)
  expect_true(desc >= 0)
  # Descent matches the bbox_d field exposed for grob-to-grob alignment
  expect_equal(desc, g$bbox_d, tolerance = 1e-6)
})

test_that("editGrob keeps viewport just in sync with hjust/vjust", {
  g <- latex_grob("x", render_mode = "path", hjust = 0.5, vjust = 0.5)
  g2 <- grid::editGrob(g, hjust = 0, vjust = 1)
  expect_equal(g2$hjust, 0)
  expect_equal(g2$vjust, 1)
  expect_equal(as.numeric(g2$vp$valid.just), c(0, 1))
})

test_that("latex_dims respects math_font parameter", {
  pdf(NULL); on.exit(dev.off(), add = TRUE)
  expr <- "$\\int_0^1 f(x)\\,dx + x + y$"
  dims_lete <- latex_dims(expr, math_font = "lete", gp = grid::gpar(fontsize = 20))
  dims_stix <- latex_dims(expr, math_font = "stix", gp = grid::gpar(fontsize = 20))
  w_lete <- grid::convertWidth(dims_lete$width, "bigpts", valueOnly = TRUE)
  w_stix <- grid::convertWidth(dims_stix$width, "bigpts", valueOnly = TRUE)
  expect_true(w_lete > 0)
  expect_true(w_stix > 0)
  expect_false(w_lete == w_stix)
})

# --- \def command ---

test_that("\\def defines a zero-argument macro and renders identically to \\newcommand", {
  # \def\mymacroA{x^2} should produce the same layout as \newcommand{\mymacroB}{x^2}
  layout_nc  <- parse_latex_cpp("\\newcommand{\\mymacroB}{x^2} \\mymacroB", text_size = 20)
  layout_def <- parse_latex_cpp("\\def\\mymacroA{x^2} \\mymacroA",           text_size = 20)
  expect_equal(nrow(layout_def), nrow(layout_nc))
})

test_that("\\def silently overwrites an existing macro, and the last wins", {
  # \newcommand errors on redefinition; \def replaces. Asserting only that
  # it does not error would pass if the *first* body were kept.
  got <- parse_latex_cpp(
    "\\def\\myoverwrite{x} \\def\\myoverwrite{y} \\myoverwrite", text_size = 20)
  expect_equal(got, parse_latex_cpp("y", text_size = 20))
  expect_false(isTRUE(all.equal(got, parse_latex_cpp("x", text_size = 20))))
})

test_that("\\def with invalid control sequence name throws a parse error", {
  expect_error(
    parse_latex_cpp("\\def{notacontrolseq}{body}", text_size = 20)
  )
})

test_that("\\def with sequential #1..#N parameters expands like \\newcommand[N]", {
  # Single-arg form: \def\sq#1{#1^2} should match \newcommand{\sq}[1]{#1^2}
  layout_nc1  <- parse_latex_cpp("\\newcommand{\\sqB}[1]{#1^2} \\sqB{a}", text_size = 20)
  layout_def1 <- parse_latex_cpp("\\def\\sqA#1{#1^2} \\sqA{a}",           text_size = 20)
  expect_equal(nrow(layout_def1), nrow(layout_nc1))

  # Two-arg form: \def\pair#1#2{#1+#2}
  layout_nc2  <- parse_latex_cpp("\\newcommand{\\pairB}[2]{#1+#2} \\pairB{a}{b}", text_size = 20)
  layout_def2 <- parse_latex_cpp("\\def\\pairA#1#2{#1+#2} \\pairA{a}{b}",         text_size = 20)
  expect_equal(nrow(layout_def2), nrow(layout_nc2))
})

test_that("\\def rejects non-sequential or malformed parameter patterns", {
  # Out-of-order parameters: #2 before #1
  expect_error(
    parse_latex_cpp("\\def\\bad#2#1{#1#2}", text_size = 20)
  )
  # Skipping a parameter: #1 then #3
  expect_error(
    parse_latex_cpp("\\def\\skip#1#3{#1#3}", text_size = 20)
  )
  # '#' not followed by a digit
  expect_error(
    parse_latex_cpp("\\def\\noarg#x{x}", text_size = 20)
  )
})

test_that("the typeface fallback warns once per device, not once per grob", {
  # The condition belongs to the device, but makeContent() runs per grob and
  # on every redraw, so a figure with several labels used to raise the same
  # warning once per label.
  tf <- tempfile(fileext = ".ps")
  grDevices::postscript(tf)
  on.exit({ grDevices::dev.off(); unlink(tf) }, add = TRUE)
  gridmicrotex:::.clear_typeface_noted()

  draw <- function() {
    g <- latex_grob("\frac{a}{b}", render_mode = "typeface",
                    gp = grid::gpar(fontsize = 20))
    grid::grid.newpage()
    grid::grid.draw(g)
  }
  w <- character(0)
  withCallingHandlers(
    for (i in 1:5) draw(),
    message = function(cond) {
      w <<- c(w, conditionMessage(cond))
      invokeRestart("muffleMessage")
    }
  )
  fallback <- grep("falling back to path mode", w, value = TRUE)
  expect_length(fallback, 1L)

  # A different device is a different answer, so it is told too.
  tf2 <- tempfile(fileext = ".ps")
  grDevices::postscript(tf2)
  w2 <- character(0)
  withCallingHandlers(draw(), message = function(cond) {
    w2 <<- c(w2, conditionMessage(cond)); invokeRestart("muffleMessage")
  })
  grDevices::dev.off(); unlink(tf2)
  expect_length(grep("falling back to path mode", w2, value = TRUE), 1L)
})

test_that("the fallback is reported only when typeface was asked for", {
  # render_mode defaults to "typeface", so everyone lands in the fallback on
  # a device without glyphs. Reporting that would reach users who never
  # expressed a preference; only an unmet *explicit* request is worth a word.
  msgs <- function(expr) {
    got <- character(0)
    withCallingHandlers(expr, message = function(cond) {
      got <<- c(got, conditionMessage(cond)); invokeRestart("muffleMessage")
    })
    grep("falling back to path mode", got, value = TRUE)
  }
  on_ps <- function(expr) {
    f <- tempfile(fileext = ".ps")
    grDevices::postscript(f)
    on.exit({ grDevices::dev.off(); unlink(f) }, add = TRUE)
    gridmicrotex:::.clear_typeface_noted()
    msgs({ grid::grid.newpage(); expr() })
  }

  # asked for it explicitly -> told once
  expect_length(on_ps(function()
    grid.latex("x^2", render_mode = "typeface", gp = grid::gpar(fontsize = 20))), 1L)

  # inherited the default -> silent
  expect_length(on_ps(function()
    grid.latex("x^2", gp = grid::gpar(fontsize = 20))), 0L)

  # set globally via latex_options() counts as asking
  old <- latex_options(render_mode = "typeface")
  on.exit(reset_latex_options(), add = TRUE)
  expect_length(on_ps(function()
    grid.latex("x^2", gp = grid::gpar(fontsize = 20))), 1L)
})
