test_that("create_nodes and create_edges build valid core objects", {
  meta <- data.frame(
    sample_id = c("S1", "S2"),
    subject_id = c("P1", "P2"),
    batch_id = c("B1", "B1"),
    study_id = c("ST1", "ST1"),
    stringsAsFactors = FALSE
  )

  samples <- create_nodes(meta, type = "Sample", id_col = "sample_id")
  subjects <- create_nodes(meta, type = "Subject", id_col = "subject_id")
  edges <- create_edges(meta, "sample_id", "subject_id", "Sample", "Subject", "sample_belongs_to_subject")

  graph <- build_dependency_graph(list(samples, subjects), list(edges))
  validation <- validate_graph(graph)

  expect_s3_class(samples, "graph_node_set")
  expect_s3_class(edges, "graph_edge_set")
  expect_s3_class(graph, "dependency_graph")
  expect_true(validation$valid)
  expect_named(samples$data$attrs[[1]], c("subject_id", "batch_id", "study_id"))
  expect_equal(igraph::vcount(as_igraph(graph)), nrow(graph$nodes$data))
  expect_equal(igraph::ecount(as_igraph(graph)), nrow(graph$edges$data))
})

test_that("compatibility aliases delegate to the primary constructors", {
  meta <- data.frame(
    sample_id = c("S1", "S2"),
    subject_id = c("P1", "P2"),
    stringsAsFactors = FALSE
  )

  samples <- create_nodes(meta, type = "Sample", id_col = "sample_id")
  subjects <- new_depgraph_nodes(create_nodes(meta, type = "Subject", id_col = "subject_id")$data)
  edges <- new_depgraph_edges(
    create_edges(meta, "sample_id", "subject_id", "Sample", "Subject", "sample_belongs_to_subject")$data
  )
  full_nodes <- new_depgraph_nodes(rbind(samples$data, subjects$data))

  graph <- new_depgraph(nodes = full_nodes, edges = edges)
  graph2 <- build_depgraph(nodes = list(samples, subjects), edges = list(edges))

  expect_s3_class(subjects, "graph_node_set")
  expect_s3_class(edges, "graph_edge_set")
  expect_s3_class(graph, "dependency_graph")
  expect_s3_class(graph2, "dependency_graph")
  expect_equal(igraph::vcount(as_igraph(graph2)), 4)
})

test_that("build_dependency_graph stores validation overrides in metadata", {
  meta <- data.frame(
    sample_id = c("S1"),
    subject_id = c("P1"),
    stringsAsFactors = FALSE
  )

  samples <- create_nodes(meta, type = "Sample", id_col = "sample_id")
  subjects <- create_nodes(meta, type = "Subject", id_col = "subject_id")
  edges <- create_edges(meta, "sample_id", "subject_id", "Sample", "Subject", "sample_belongs_to_subject")

  graph <- build_dependency_graph(
    list(samples, subjects),
    list(edges),
    validation_overrides = list(allow_multi_subject_samples = TRUE)
  )

  expect_true(isTRUE(graph$metadata$validation_overrides$allow_multi_subject_samples))
})

test_that("create_edges can target unprefixed node identifiers when requested", {
  meta <- data.frame(
    sample_id = "S1",
    subject_id = "P1",
    stringsAsFactors = FALSE
  )

  samples <- create_nodes(meta, type = "Sample", id_col = "sample_id", prefix = FALSE)
  subjects <- create_nodes(meta, type = "Subject", id_col = "subject_id")
  edges <- create_edges(
    meta,
    "sample_id",
    "subject_id",
    "Sample",
    "Subject",
    "sample_belongs_to_subject",
    from_prefix = FALSE
  )

  graph <- build_dependency_graph(list(samples, subjects), list(edges))

  expect_s3_class(graph, "dependency_graph")
  expect_true(validate_graph(graph)$valid)
  expect_equal(graph$edges$data$from, "S1")
  expect_equal(graph$edges$data$to, "subject:P1")
})

test_that("create_nodes collapses exact duplicate definitions deterministically", {
  nodes <- create_nodes(
    data.frame(
      subject_id = c("P1", "P1"),
      subject_type = c("case", "case"),
      stringsAsFactors = FALSE
    ),
    type = "Subject",
    id_col = "subject_id",
    attr_cols = "subject_type"
  )

  expect_s3_class(nodes, "graph_node_set")
  expect_equal(nrow(nodes$data), 1L)
  expect_equal(nodes$data$node_id, "subject:P1")
  expect_identical(nodes$data$attrs[[1]]$subject_type, "case")
})

test_that("create_nodes rejects conflicting duplicate definitions", {
  expect_error(
    create_nodes(
      data.frame(
        subject_id = c("P1", "P1"),
        subject_type = c("case", "control"),
        stringsAsFactors = FALSE
      ),
      type = "Subject",
      id_col = "subject_id",
      attr_cols = "subject_type"
    ),
    "Conflicting node definitions found for IDs",
    fixed = TRUE
  )
})

test_that("create_edges collapses exact duplicate definitions deterministically", {
  edges <- create_edges(
    data.frame(
      sample_id = c("S1", "S1"),
      subject_id = c("P1", "P1"),
      note = c("same", "same"),
      stringsAsFactors = FALSE
    ),
    from_col = "sample_id",
    to_col = "subject_id",
    from_type = "Sample",
    to_type = "Subject",
    relation = "sample_belongs_to_subject",
    attr_cols = "note"
  )

  expect_s3_class(edges, "graph_edge_set")
  expect_equal(nrow(edges$data), 1L)
  expect_equal(edges$data$from, "sample:S1")
  expect_equal(edges$data$to, "subject:P1")
  expect_identical(edges$data$attrs[[1]]$note, "same")
})

test_that("create_edges rejects conflicting duplicate definitions", {
  expect_error(
    create_edges(
      data.frame(
        sample_id = c("S1", "S1"),
        subject_id = c("P1", "P1"),
        note = c("first", "second"),
        stringsAsFactors = FALSE
      ),
      from_col = "sample_id",
      to_col = "subject_id",
      from_type = "Sample",
      to_type = "Subject",
      relation = "sample_belongs_to_subject",
      attr_cols = "note"
    ),
    "Conflicting edge definitions found for relations",
    fixed = TRUE
  )
})

test_that("conflicting duplicate timepoint definitions cannot change derived ordering", {
  make_graph <- function(time_index_values) {
    meta <- data.frame(
      sample_id = c("S1", "S2"),
      timepoint_id = c("T1", "T2"),
      stringsAsFactors = FALSE
    )

    sample_nodes <- create_nodes(meta, type = "Sample", id_col = "sample_id")
    time_nodes <- create_nodes(
      data.frame(
        timepoint_id = c("T1", "T1", "T2"),
        time_index = time_index_values,
        stringsAsFactors = FALSE
      ),
      type = "Timepoint",
      id_col = "timepoint_id",
      attr_cols = "time_index"
    )
    time_edges <- create_edges(
      meta,
      "sample_id",
      "timepoint_id",
      "Sample",
      "Timepoint",
      "sample_collected_at_timepoint"
    )

    build_dependency_graph(list(sample_nodes, time_nodes), list(time_edges))
  }

  expect_error(
    make_graph(c(1, 9, 2)),
    "Conflicting node definitions found for IDs",
    fixed = TRUE
  )
  expect_error(
    make_graph(c(9, 1, 2)),
    "Conflicting node definitions found for IDs",
    fixed = TRUE
  )
})

test_that("low-level constructors reject malformed attrs", {
  expect_error(
    graph_node_set(
      data.frame(
        node_id = "sample:S1",
        node_type = "Sample",
        node_key = "S1",
        label = "S1",
        attrs = I(list(list("bad"))),
        stringsAsFactors = FALSE
      )
    ),
    "named lists"
  )

  expect_error(
    graph_edge_set(
      data.frame(
        edge_id = "sample_belongs_to_subject:1",
        from = "sample:S1",
        to = "subject:P1",
        edge_type = "sample_belongs_to_subject",
        attrs = I(list(list("bad"))),
        stringsAsFactors = FALSE
      )
    ),
    "named lists"
  )
})

test_that("dependency_graph rejects igraph objects that do not match node and edge tables", {
  nodes <- graph_node_set(
    data.frame(
      node_id = c("sample:S1", "subject:P1"),
      node_type = c("Sample", "Subject"),
      node_key = c("S1", "P1"),
      label = c("S1", "P1"),
      attrs = I(list(list(), list())),
      stringsAsFactors = FALSE
    )
  )

  edges <- graph_edge_set(
    data.frame(
      edge_id = "sample_belongs_to_subject:1",
      from = "sample:S1",
      to = "subject:P1",
      edge_type = "sample_belongs_to_subject",
      attrs = I(list(list())),
      stringsAsFactors = FALSE
    )
  )

  bad_graph <- igraph::make_empty_graph(n = 1, directed = TRUE)
  igraph::V(bad_graph)$name <- "sample:S1"

  expect_error(
    dependency_graph(nodes = nodes, edges = edges, graph = bad_graph),
    "does not match the supplied node and edge tables",
    fixed = TRUE
  )
})
