# Tests for `ptr_init_state`, `ptr_server_internal` observers, and the
# `ptr_extract_*` accessors. Reactivity tests use `shiny::testServer`.

.server_test_env <- function(extras = list()) {
  list2env(c(list(mtcars = mtcars), extras), parent = globalenv())
}

# ---- Pure state shape ----

test_that("ptr_init_state returns a typed-state structure", {
  state <- ptr_init_state(
    "ggplot(mtcars) + geom_point()",
    envir = .server_test_env()
  )
  expect_s3_class(state, "ptr_state")
  expect_s3_class(state, "ptr_state")
  expect_true(is.function(state$tree))  # reactiveVal is a function
})

test_that("ptr_init_state honors ppLayerOff(geom, TRUE) via node$default_active", {
  # ADR 0020 / Plan 04: replaces the deleted `checkbox_defaults = ...`
  # test. The formula-side `ppLayerOff()` now carries the bit; the layer's
  # default-checked state is read from `node$default_active` (stamped on
  # the carrier in `ptr_translate()`).
  tree <- ptr_translate(
    "ggplot(mtcars) + ppLayerOff(geom_smooth(), TRUE) + geom_point()"
  )
  smooth_layer <- tree$layers[[which(vapply(
    tree$layers, function(l) identical(l$name, "geom_smooth"), logical(1)
  ))]]
  point_layer <- tree$layers[[which(vapply(
    tree$layers, function(l) identical(l$name, "geom_point"), logical(1)
  ))]]
  expect_false(isTRUE(smooth_layer$default_active %||% TRUE))
  expect_true(isTRUE(point_layer$default_active %||% TRUE))
})

test_that("ptr_init_state builds resolved_data slots only for bare-data-source layers", {
  # Pipeline-data layers no longer carry a click-gated cache; their
  # consumers resolve lazily through `ptr_setup_consumer_uis()`. Only
  # bare-data-source layers (e.g., `ggplot(data = upload, ...)`) keep a
  # `resolved_data` slot for the upload reactive observer to write into.
  state <- ptr_init_state(
    "mtcars |> head(ppNum) |> ggplot() + geom_point()",
    envir = .server_test_env()
  )
  expect_equal(length(state$resolved_data), 0L)
})

test_that("ptr_init_state with literal-data layers has empty resolved_data", {
  state <- ptr_init_state(
    "ggplot(mtcars) + geom_point()",
    envir = .server_test_env()
  )
  expect_equal(length(state$resolved_data), 0L)
})

test_that("ptr_init_state rejects non-function ns", {
  expect_error(
    ptr_init_state("ggplot(mtcars)", ns = "module1"),
    "namespace function"
  )
})

# ---- runtime ----

test_that("runtime observer populates state$runtime with code_text and plot", {
  e <- .server_test_env()
  server <- function(input, output, session) {
    session$userData$state <- ptr_server_internal(
      input, output, session,
      "ggplot(mtcars, aes(x = mpg, y = hp)) + geom_point()",
      envir = e
    )
  }
  shiny::testServer(server, {
    state <- session$userData$state
    session$setInputs(ptr_update_plot = 1L)  # gated on Update Plot click
    res <- state$runtime()
    expect_true(isTRUE(res$ok), info = res$error %||% "ok")
    expect_match(res$code_text, "ggplot")
    expect_s3_class(res$plot, "ggplot")
  })
})

# ---- ptr_gg_extra ----

test_that("P12.10 ptr_gg_extra captures extras", {
  e <- .server_test_env()
  state <- ptr_init_state("ggplot(mtcars) + geom_point()", envir = e)
  shiny::isolate({
    ptr_gg_extra(state, ggplot2::scale_x_log10())
    expect_equal(length(state$extras()), 1L)
  })
})

test_that("P12.11 ptr_gg_extra no-op when no args", {
  e <- .server_test_env()
  state <- ptr_init_state("ggplot(mtcars) + geom_point()", envir = e)
  shiny::isolate({
    ptr_gg_extra(state)
    expect_equal(length(state$extras()), 0L)
  })
})

test_that("P12.12 ptr_gg_extra leaves extras untouched on eval failure", {
  e <- .server_test_env()
  state <- ptr_init_state("ggplot(mtcars) + geom_point()", envir = e)
  shiny::isolate({
    expect_error(
      ptr_gg_extra(state, does_not_exist_xyz()),
      "does_not_exist_xyz"
    )
    expect_equal(length(state$extras()), 0L)
  })
})

# ---- extractors ----

test_that("ptr_extract_* surface the runtime fields", {
  e <- .server_test_env()
  server <- function(input, output, session) {
    session$userData$state <- ptr_server_internal(
      input, output, session,
      "ggplot(mtcars, aes(x = mpg, y = hp)) + geom_point()",
      envir = e
    )
  }
  shiny::testServer(server, {
    state <- session$userData$state
    session$setInputs(ptr_update_plot = 1L)  # gated on Update Plot click
    expect_s3_class(ptr_extract_plot(state), "ggplot")
    expect_match(ptr_extract_code(state), "geom_point")
    expect_null(ptr_extract_error(state))
  })
})

test_that("inject_resolved_data swaps data_arg for the cached frame (spec L105)", {
  # Pruned tree: ggplot(data = mtcars) layer + geom_point() layer. After
  # injection with a 3-row cache for the ggplot layer, that layer's
  # data_arg is a ptr_literal carrying the cached frame; geom_point has
  # no cache entry and is left intact.
  r <- ptr_translate("ggplot(data = mtcars, aes(x = mpg)) + geom_point()")
  r <- ptr_classify_data(r)
  r <- ptr_assign_ids(r)
  r <- ptr_shared_bind(r)
  pruned <- ptr_prune(ptr_substitute(r, input_snapshot = list()))

  fake_state <- list(
    resolved_data = list(ggplot = function() utils::head(mtcars, 3))
  )
  injected <- inject_resolved_data(pruned, fake_state)

  ggplot_layer <- injected$layers[[1L]]
  expect_s3_class(ggplot_layer$data_arg, "ptr_literal")
  expect_s3_class(ggplot_layer$data_arg$expr, "data.frame")
  expect_equal(nrow(ggplot_layer$data_arg$expr), 3L)

  # Layers without a cache entry are untouched.
  geom_layer <- injected$layers[[2L]]
  expect_null(geom_layer$data_arg)
})

# ---- ptr_validate_state guard ----

test_that("ptr_register_* abort on a malformed state", {
  # ptr_register_* are internal (un-exported) post-rewrite; sole caller is
  # ptr_server_internal() via ptr_setup_runtime(). Reach them with ::: for this guard.
  expect_error(ggpaintr:::ptr_register_plot(list(), list()), "ptr_state|missing required")
  expect_error(ggpaintr:::ptr_register_error(list(), list()), "ptr_state|missing required")
  expect_error(ggpaintr:::ptr_register_code(list(), list()), "ptr_state|missing required")
  expect_error(ggpaintr:::ptr_register_plot(list(), "not a list"), "must be a `ptr_state`")
})

test_that("a well-formed ptr_init_state passes ptr_validate_state", {
  state <- ptr_init_state(
    "ggplot(mtcars) + geom_point()",
    envir = .server_test_env()
  )
  expect_true(ptr_validate_state(state))
})

# ---- runtime observer dependency: no `||` short-circuit ----

test_that("runtime re-runs on a post-Update-Plot extras change", {
  # Regression: `triggered()` in ptr_setup_runtime() must read ALL of
  # `input$ptr_update_plot`, `draw_trigger`, and `extras` on every call --
  # not let `||` short-circuit after the Update Plot click. Otherwise the
  # runtime observer never establishes a dependency on `extras` /
  # `draw_trigger`, so `ptr_gg_extra()` changes (and the grid app's
  # "Draw all" trigger) stop redrawing a panel once its own Update Plot
  # has fired. `extras` and `draw_trigger` share that root cause; testing
  # the `extras` branch guards both.
  e <- .server_test_env()
  server <- function(input, output, session) {
    session$userData$state <- ptr_server_internal(
      input, output, session,
      "ggplot(mtcars, aes(x = mpg, y = hp)) + geom_point()",
      envir = e
    )
  }
  shiny::testServer(server, {
    state <- session$userData$state
    session$setInputs(ptr_update_plot = 1L)
    rt_before <- state$runtime()
    expect_false(is.null(rt_before))

    ptr_gg_extra(state, ggplot2::theme_void())
    session$flushReact()
    rt_after <- state$runtime()
    # Observer re-ran -> fresh runtime result object.
    expect_false(identical(rt_before, rt_after))
  })
})
