# Helpers shared with test-rewrite-build-ui.R
.find_tags2 <- function(tag, has_class = NULL, has_id = NULL) {
  out <- list()
  visit <- function(x) {
    if (inherits(x, "shiny.tag")) {
      cls <- x$attribs$class %||% ""
      id <- x$attribs$id %||% ""
      class_match <- is.null(has_class) || grepl(has_class, cls, fixed = TRUE)
      id_match <- is.null(has_id) || identical(id, has_id)
      if (class_match && id_match) out[[length(out) + 1L]] <<- x
      for (child in x$children) visit(child)
    } else if (inherits(x, "shiny.tag.list") || is.list(x)) {
      for (el in x) visit(el)
    }
  }
  visit(tag)
  out
}

.layer_by_name <- function(tree, nm) {
  for (l in tree$layers) {
    if (identical(l$name, nm)) return(l)
  }
  NULL
}

# ---- P6.6 / P6.7 — Data sub-tab presence gated on pipeline placeholders ----

test_that("P6.6 layer panel includes Data sub-tab when pipeline placeholders present", {
  tree <- ptr_translate("mtcars |> head(ppNum) |> ggplot(aes(x = ppVar))")
  layer <- .layer_by_name(tree, "ggplot")
  panel <- build_ui_for(layer)
  # Look for tabsetPanel containing two tabPanels (Data + Controls)
  rendered <- as.character(panel)
  expect_match(rendered, "Data")
  expect_match(rendered, "Controls")
})

test_that("P6.7 layer panel omits Data sub-tab when no pipeline placeholders", {
  tree <- ptr_translate("ggplot(mtcars, aes(x = ppVar)) + geom_point()")
  layer <- .layer_by_name(tree, "ggplot")
  panel <- build_ui_for(layer)
  rendered <- as.character(panel)
  # No Data sub-tab should be emitted; controls flat
  # We assert by checking no nested tabsetPanel structure (heuristic)
  expect_no_match(rendered, ">Data<")  # tab title surrounded by tags
})

# ---- P6.8 / P6.12 — checkbox emission ----

test_that("P6.8 non-ggplot layer panel includes a checkbox", {
  tree <- ptr_translate("ggplot(mtcars) + geom_point()")
  layer <- .layer_by_name(tree, "geom_point")
  panel <- build_ui_for(layer)
  cb_id <- "geom_point_checkbox"
  expect_true(length(.find_tags2(panel, has_id = cb_id)) > 0L)
})

test_that("P6.12 ggplot layer has no checkbox", {
  tree <- ptr_translate("ggplot(mtcars) + geom_point()")
  layer <- .layer_by_name(tree, "ggplot")
  panel <- build_ui_for(layer)
  expect_equal(length(.find_tags2(panel, has_id = "ggplot_checkbox")), 0L)
})

# ---- P6.9 / P6.10 — checkbox default state ----

test_that("P6.9 checkbox defaults to TRUE when ppLayerOff is not used", {
  # ADR 0020: default-active reads from `node$default_active`. A bare
  # geom_point() leaves the field unset, so the carrier's effective value
  # is TRUE -> the static panel renders the checkbox `checked="checked"`.
  tree <- ptr_translate("ggplot(mtcars) + geom_point()")
  layer <- .layer_by_name(tree, "geom_point")
  panel <- build_ui_for(layer)
  rendered <- as.character(panel)
  expect_match(rendered, 'checked="checked"', fixed = TRUE)
})

test_that("P6.10 checkbox defaults to FALSE via ppLayerOff(geom, TRUE)", {
  # ADR 0020 / Plan 04 rewrite of the deleted `checkbox_defaults =` test.
  # `ppLayerOff(geom_point(), TRUE)` stamps `default_active = FALSE` on
  # the carrier ptr_layer; `build_ui_for.ptr_layer` reads that field
  # directly to set the checkbox state and the disabled-class on the
  # layer content wrapper.
  tree <- ptr_translate("ggplot(mtcars) + ppLayerOff(geom_point(), TRUE)")
  layer <- .layer_by_name(tree, "geom_point")
  panel <- build_ui_for(layer)
  rendered <- as.character(panel)
  expect_no_match(rendered, 'checked="checked"', fixed = TRUE)
})

# ---- P6.11 — content div carries ptr-layer-disabled when FALSE ----

test_that("P6.11 ppLayerOff(geom, TRUE) adds ptr-layer-disabled class", {
  # ADR 0020 / Plan 04 rewrite: same source-of-truth as P6.10 above; the
  # static panel's content div picks up `ptr-layer-disabled` when the
  # carrier's `default_active` is FALSE.
  tree <- ptr_translate("ggplot(mtcars) + ppLayerOff(geom_point(), TRUE)")
  layer <- .layer_by_name(tree, "geom_point")
  panel <- build_ui_for(layer)
  disabled_divs <- .find_tags2(panel, has_class = "ptr-layer-disabled")
  expect_true(length(disabled_divs) > 0L)
})

test_that("active layer content div has ptr-layer-content but not -disabled", {
  tree <- ptr_translate("ggplot(mtcars) + geom_point()")
  layer <- .layer_by_name(tree, "geom_point")
  panel <- build_ui_for(layer)  # default = TRUE
  content_divs <- .find_tags2(panel, has_class = "ptr-layer-content")
  disabled_divs <- .find_tags2(panel, has_class = "ptr-layer-disabled")
  expect_true(length(content_divs) > 0L)
  expect_equal(length(disabled_divs), 0L)
})

# ---- ns_fn threading ----

test_that("layer panel namespaces ids via ns_fn", {
  tree <- ptr_translate("ggplot(mtcars) + geom_point()")
  layer <- .layer_by_name(tree, "geom_point")
  ns <- shiny::NS("module1")
  panel <- build_ui_for(layer, ns_fn = ns)
  expect_true(length(.find_tags2(panel, has_id = ns("geom_point_checkbox"))) > 0L)
})

# ---- Phase 4.1 — pipeline-stage placeholder labels name the verb ----

test_that("pipeline-stage placeholder label names the verb via {param}", {
  # ADR 0012 / PLAN-01 (Bug B): the widget label now resolves inside the
  # renderUI body in `ptr_setup_value_uis()` (the static layer panel only
  # holds a uiOutput container). The label assertion drives testServer +
  # `session$getOutput()` to capture the post-flush HTML.
  formula <- "mtcars |> head(ppNum) |> ggplot(aes(x = ppVar))"
  html <- .render_widget_html(formula, raw_id = "ggplot_2_1_ppNum_NA")
  # Default copy is "Enter a number for {param}"; for an unnamed positional arg
  # the verb fills {param}, giving "...for head()" -- and we do NOT also append
  # " in head()" on top of that.
  expect_match(html, "Enter a number for head\\(\\)")
  expect_no_match(html, "head\\(\\) in head\\(\\)")
})

test_that("unnamed-arg pipeline placeholder uses 'verb()' as the copy param key", {
  # Hybrid: the stage-group header ("<code>head()</code>") is composed in
  # the static layer panel via `controllable_region()`, so `build_ui_for`
  # still observes it. The widget's resolved label (custom override) lives
  # in the renderUI -> `.render_widget_html`.
  formula <- "mtcars |> head(ppNum) |> ggplot(aes(x = ppVar))"
  tree <- ptr_translate(formula)
  layer <- .layer_by_name(tree, "ggplot")
  static_panel <- as.character(build_ui_for(layer))
  expect_match(static_panel, "<code>head\\(\\)</code>")

  ui_text <- list(params = list(`head()` = list(ppNum = list(label = "How many rows"))))
  widget_html <- .render_widget_html(formula, raw_id = "ggplot_2_1_ppNum_NA", ui_text = ui_text)
  # the `head()` param key still resolves the custom label ...
  expect_match(widget_html, "How many rows")
  # ... but the verb is named once, by the stage-group header -- not as a
  # per-widget " in head()" suffix.
  expect_no_match(widget_html, "How many rows in head")
})

test_that("a pipeline placeholder's widget no longer carries the ' in verb()' suffix", {
  formula <- "mtcars |> transform(n = ppNum) |> ggplot(aes(x = ppVar))"
  tree <- ptr_translate(formula)
  layer <- .layer_by_name(tree, "ggplot")
  static_panel <- as.character(build_ui_for(layer))
  # Stage-group scaffolding from the static panel.
  expect_match(static_panel, "ptr-stage-head")
  expect_match(static_panel, "<code>transform\\(\\)</code>")
  # The widget label itself (composed inside the renderUI body) does NOT
  # carry the redundant " in transform()" suffix.
  widget_html <- .render_widget_html(formula, raw_id = "ggplot_2_1_ppNum_NA")
  expect_no_match(widget_html, "in transform\\(\\)")
})

# ---- pipeline-stage groups ----

test_that("pipeline stages render as .ptr-stage groups with a verb-labelled checkbox", {
  tree <- ptr_translate(
    "mtcars |> subset(mpg > ppNum) |> head(ppNum) |> ggplot(aes(x = ppVar))"
  )
  layer <- .layer_by_name(tree, "ggplot")
  panel <- build_ui_for(layer)
  expect_equal(length(.find_tags2(panel, has_class = "ptr-stage-head")), 2L)
  expect_true(length(.find_tags2(panel, has_class = "ptr-stage-fields")) >= 2L)
  txt <- as.character(panel)
  expect_match(txt, "<code>subset\\(\\)</code>")
  expect_match(txt, "<code>head\\(\\)</code>")
  # the stage checkboxes are wired on the stage_id input
  expect_match(txt, "stage_enabled")
})

test_that("placeholder nested in a sub-expression names the stage verb, not the inner call", {
  # `text` is the RHS of `Species == text`, itself an argument to `subset()`.
  # The label/copy key must report the stage verb `subset()`, never `==()`.
  # ADR 0012 / PLAN-01 (Bug B): widget label resolves inside the renderUI
  # body in `ptr_setup_value_uis()`; assert via `.render_widget_html`.
  formula <- "iris |> subset(Species == ppText) |> ggplot(aes(x = ppVar))"
  html <- .render_widget_html(formula, raw_id = "ggplot_2_1_2_ppText_NA",
                              envir = list2env(list(iris = iris), parent = globalenv()))
  expect_match(html, "Enter a value for subset\\(\\)")
  expect_no_match(html, "==\\(\\)")
})

# ---- Phase 3 — layer-disabled visual cue ----

test_that("app shells inject the structural-layer dependency", {
  f <- "ggplot(mtcars, aes(x = mpg, y = hp)) + geom_point()"
  for (ui in list(ptr_app_components(f)$ui, ptr_ui(f, "m"))) {
    rendered <- render_with_deps(ui)
    # ptr_set_class + .ptr-layer-disabled now ship in ggpaintr-layer.{js,css}
    # as an htmlDependency rather than inline tags.
    expect_match(rendered, "ggpaintr-layer.js", fixed = TRUE)
    expect_match(rendered, "ggpaintr-layer.css", fixed = TRUE)
  }
  # bslib path includes only this dependency; assert the helper emits it.
  assets <- render_with_deps(shiny::tagList(ptr_layer_assets()))
  expect_match(assets, "ggpaintr-layer.js", fixed = TRUE)
  expect_match(assets, "ggpaintr-layer.css", fixed = TRUE)
})

test_that("toggling a layer include-checkbox sends ptr_set_class", {
  e <- list2env(list(mtcars = mtcars), parent = globalenv())
  sent <- new.env(parent = emptyenv())
  sent$msgs <- list()
  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$sendCustomMessage <- function(type, message) {
      sent$msgs[[length(sent$msgs) + 1L]] <- list(type = type, message = message)
    }
    spec <- state$input_spec
    ck <- spec$input_id[spec$role == "layer_checkbox"][[1]]
    content_id <- layer_panel_content_id(
      spec$layer_name[spec$role == "layer_checkbox"][[1]]
    )

    do.call(session$setInputs, stats::setNames(list(FALSE), ck))
    session$flushReact()
    off_msgs <- Filter(function(m) identical(m$type, "ptr_set_class") &&
                         identical(m$message$id, content_id), sent$msgs)
    expect_true(length(off_msgs) >= 1L)
    expect_true(isTRUE(off_msgs[[length(off_msgs)]]$message$add))
    expect_equal(off_msgs[[length(off_msgs)]]$message$cls, "ptr-layer-disabled")

    do.call(session$setInputs, stats::setNames(list(TRUE), ck))
    session$flushReact()
    on_msgs <- Filter(function(m) identical(m$type, "ptr_set_class") &&
                        identical(m$message$id, content_id), sent$msgs)
    expect_false(isTRUE(on_msgs[[length(on_msgs)]]$message$add))
  })
})
