app_directory <- Sys.getenv("CASCADE_APP_DIR", unset = normalizePath(file.path("..", "..")))
source(file.path(app_directory, "R", "helpers.R"), local = TRUE)
source(file.path(app_directory, "R", "plots.R"), local = TRUE)

plot_customdata <- function(built_plot) {
  unlist(lapply(built_plot$x$data, function(trace) trace$customdata), use.names = FALSE)
}

plot_y_values <- function(built_plot) {
  unlist(lapply(built_plot$x$data, function(trace) trace$y), use.names = FALSE)
}

expect_all_families_in_hover <- function(built_plot) {
  hovers <- plot_customdata(built_plot)
  expect_true(all(vapply(
    unname(network_labels),
    function(label) any(grepl(label, hovers, fixed = TRUE)),
    logical(1)
  )))
}

test_that("density, centrality-metric, and threshold contrasts show all three graph families", {
  data <- readRDS(file.path(app_directory, "data", "cascade_results_explorer.rds"))

  snapshots <- list(
    density = subset(
      data$table2,
      metric == "strength" & threshold == "0.20-0.40"
    ),
    centrality_metric = subset(
      data$table2,
      density_condition == "Denser" & threshold == "0.20-0.40"
    ),
    threshold = subset(
      data$table2,
      density_condition == "Denser" & metric == "strength"
    )
  )
  dimensions <- c(
    density = "density_condition",
    centrality_metric = "metric",
    threshold = "threshold"
  )

  expect_equal(vapply(snapshots, nrow, integer(1)), c(
    density = 6L,
    centrality_metric = 12L,
    threshold = 12L
  ))

  built <- Map(
    function(snapshot, dimension) {
      plotly::plotly_build(plot_multifamily_contrast_snapshot(snapshot, dimension))
    },
    snapshots,
    dimensions
  )

  expect_true(all(vapply(built, function(plot) length(plot$x$data) == 3L, logical(1))))
  expect_true(all(vapply(built, function(plot) all(is.finite(plot_y_values(plot))), logical(1))))
  expect_equal(built$centrality_metric$x$layout$xaxis$title$text, "Centrality metric")
  expect_false(grepl(
    "definition",
    built$centrality_metric$x$layout$xaxis$title$text,
    ignore.case = TRUE
  ))
  invisible(lapply(built, expect_all_families_in_hover))
})

test_that("density and threshold extent plots show three families by two target levels", {
  data <- readRDS(file.path(app_directory, "data", "cascade_results_explorer.rds"))

  density <- subset(
    data$cascade_summary,
    metric == "strength" & threshold == "0.20-0.40" & target %in% c("Low", "High")
  )
  threshold <- subset(
    data$cascade_summary,
    density_condition == "Denser" & metric == "strength" & target %in% c("Low", "High")
  )
  expect_equal(nrow(density), 12L)
  expect_equal(nrow(threshold), 24L)
  built <- list(
    plotly::plotly_build(plot_multifamily_extent_snapshot(density, "density_condition")),
    plotly::plotly_build(plot_multifamily_extent_snapshot(threshold, "threshold"))
  )

  expect_true(all(vapply(built, function(plot) length(plot$x$data) == 6L, logical(1))))
  expect_true(all(vapply(built, function(plot) all(is.finite(plot_y_values(plot))), logical(1))))
  invisible(lapply(built, expect_all_families_in_hover))
})

test_that("family styles repeat consistently and target styles remain distinguishable", {
  data <- readRDS(file.path(app_directory, "data", "cascade_results_explorer.rds"))
  contrast <- subset(
    data$table2,
    metric == "strength" & threshold == "0.20-0.40"
  )
  extent <- subset(
    data$cascade_summary,
    metric == "strength" & threshold == "0.20-0.40" & target %in% c("Low", "High")
  )
  contrast_built <- plotly::plotly_build(
    plot_multifamily_contrast_snapshot(contrast, "density_condition")
  )
  extent_built <- plotly::plotly_build(
    plot_multifamily_extent_snapshot(extent, "density_condition")
  )

  contrast_traces <- contrast_built$x$data
  expect_setequal(vapply(contrast_traces, `[[`, "", "name"), c("ER", "BA", "WS"))
  expect_setequal(
    vapply(contrast_traces, `[[`, "", "legendgroup"),
    names(network_labels)
  )
  expect_equal(length(unique(vapply(contrast_traces, function(trace) trace$line$color, ""))), 3L)
  expect_setequal(
    vapply(contrast_traces, function(trace) trace$marker$symbol, ""),
    c("circle", "diamond", "square")
  )

  extent_traces <- extent_built$x$data
  groups <- vapply(extent_traces, `[[`, "", "legendgroup")
  grouped_traces <- split(extent_traces, groups)
  expect_equal(as.integer(table(groups)), rep(2L, 3L))
  expect_equal(length(unique(vapply(grouped_traces, function(traces) traces[[1]]$line$color, ""))), 3L)

  for (traces in grouped_traces) {
    colors <- vapply(traces, function(trace) trace$line$color, "")
    dashes <- vapply(traces, function(trace) trace$line$dash, "")
    symbols <- vapply(traces, function(trace) trace$marker$symbol, "")
    legend_flags <- vapply(traces, function(trace) isTRUE(trace$showlegend), logical(1))

    expect_equal(length(unique(colors)), 1L)
    expect_setequal(dashes, c("dash", "solid"))
    expect_equal(length(unique(sub("-open$", "", symbols))), 1L)
    expect_true(any(grepl("-open$", symbols)))
    expect_true(any(!grepl("-open$", symbols)))
    expect_false(legend_flags[dashes == "dash"])
    expect_true(legend_flags[dashes == "solid"])
  }
})

test_that("graph-family and total-failure snapshots remain bounded and valid", {
  data <- readRDS(file.path(app_directory, "data", "cascade_results_explorer.rds"))
  graph_family <- subset(
    data$table2,
    density_condition == "Denser" & metric == "strength" & threshold == "0.20-0.40"
  )
  total_failure <- subset(
    data$cascade_summary,
    density_condition == "Denser" & metric == "strength" & threshold == "0.20-0.40" &
      target %in% c("Low", "High")
  )
  built <- list(
    plotly::plotly_build(plot_contrast_snapshot(graph_family, "network")),
    plotly::plotly_build(plot_total_failure_snapshot(total_failure, "completion")),
    plotly::plotly_build(plot_total_failure_snapshot(total_failure, "steps"))
  )

  expect_equal(length(built[[1]]$x$data), 1L)
  expect_true(all(vapply(built[-1], function(plot) length(plot$x$data) <= 2L, logical(1))))
  expect_true(all(vapply(built, function(plot) all(is.finite(plot_y_values(plot))), logical(1))))
})

test_that("public hover text stays concise across multi-family plots", {
  data <- readRDS(file.path(app_directory, "data", "cascade_results_explorer.rds"))
  density_contrast <- subset(
    data$table2,
    metric == "strength" & threshold == "0.20-0.40"
  )
  density_extent <- subset(
    data$cascade_summary,
    metric == "strength" & threshold == "0.20-0.40" & target %in% c("Low", "High")
  )
  centrality_metric <- subset(
    data$table2,
    density_condition == "Denser" & threshold == "0.20-0.40"
  )
  threshold_contrast <- subset(
    data$table2,
    density_condition == "Denser" & metric == "strength"
  )
  threshold_extent <- subset(
    data$cascade_summary,
    density_condition == "Denser" & metric == "strength" & target %in% c("Low", "High")
  )

  plots <- list(
    plot_multifamily_contrast_snapshot(density_contrast, "density_condition"),
    plot_multifamily_extent_snapshot(density_extent, "density_condition"),
    plot_multifamily_contrast_snapshot(centrality_metric, "metric"),
    plot_multifamily_contrast_snapshot(threshold_contrast, "threshold"),
    plot_multifamily_extent_snapshot(threshold_extent, "threshold")
  )
  hovers <- unlist(lapply(plots, function(plot) {
    plot_customdata(plotly::plotly_build(plot))
  }), use.names = FALSE)
  hovers <- hovers[nzchar(hovers)]

  expect_lte(max(nchar(hovers)), 220L)
  expect_lte(max(lengths(strsplit(hovers, "<br>", fixed = TRUE))), 3L)
  expect_false(any(grepl("Monte Carlo SE|Between-replication SD|Median", hovers)))
  expect_true(any(grepl("High.*low difference", hovers)))
  expect_true(any(grepl("Final activation", hovers, fixed = TRUE)))
})

test_that("zero-completion updates remain explicitly not estimable", {
  data <- readRDS(file.path(app_directory, "data", "cascade_results_explorer.rds"))
  no_completion <- subset(
    data$cascade_summary,
    threshold == "0.40-0.60" & metric == "strength" & density_condition == "Denser" &
      target %in% c("Low", "High")
  )

  built <- plotly::plotly_build(plot_total_failure_snapshot(no_completion, "steps"))
  annotation <- built$x$layout$annotations[[1]]$text

  expect_match(annotation, "not estimable", ignore.case = TRUE)
  expect_true(all(no_completion$n_complete_failures == 0L))
  expect_true(all(is.na(no_completion$mean_steps_to_complete_failure)))
})
