test_that("dfs() uses 1-based root vertex index", {
  g <- make_ring(3)
  expect_equal(dfs(g, root = 1)$root, 1)
})

test_that("dfs() does not pad order", {
  g <- make_star(3)
  expect_equal(as.numeric(dfs(g, root = 2, unreachable = FALSE)$order), c(2, 1))

  local_igraph_options(return.vs.es = FALSE)
  expect_equal(as.numeric(dfs(g, root = 2, unreachable = FALSE)$order), c(2, 1))
  expect_equal(
    as.numeric(
      dfs(g, root = 2, unreachable = FALSE, order.out = TRUE)$order.out
    ),
    c(1, 2)
  )
})

test_that("dfs() deprecated arguments", {
  g <- make_star(3)

  expect_snapshot_igraph_error({
    d <- dfs(
      g,
      root = 2,
      unreachable = FALSE,
      neimode = "out",
      father = TRUE
    )
  })
})

test_that("degree() works", {
  gnp1 <- sample_gnp(100, 1 / 100)
  gnp1_deg <- degree(gnp1)
  el <- as_edgelist(gnp1)
  expect_equal(as.numeric(table(el)), gnp1_deg[gnp1_deg != 0])

  expect_equal(gnp1_deg / (vcount(gnp1) - 1), degree(gnp1, normalized = TRUE))

  gnp2 <- sample_gnp(100, 2 / 100, directed = TRUE)
  gnp2_deg_in <- degree(gnp2, mode = "in")
  el2 <- as_edgelist(gnp2)
  expect_equal(as.numeric(table(el2[, 2])), gnp2_deg_in[gnp2_deg_in != 0])

  gnp2_deg_out <- degree(gnp2, mode = "out")
  expect_equal(as.numeric(table(el2[, 1])), gnp2_deg_out[gnp2_deg_out != 0])

  expect_equal(
    gnp2_deg_in / (vcount(gnp2) - 1),
    degree(gnp2, mode = "in", normalized = TRUE)
  )
  expect_equal(
    gnp2_deg_out / (vcount(gnp2) - 1),
    degree(gnp2, mode = "out", normalized = TRUE)
  )
  expect_equal(
    degree(gnp2, mode = "all") / (vcount(gnp2) - 1),
    degree(gnp2, mode = "all", normalized = TRUE)
  )
})

test_that("max_degree() works", {
  g <- make_graph(c(1, 2, 2, 2, 2, 3), directed = TRUE)
  expect_equal(max_degree(g), 4)
  expect_equal(max_degree(g, mode = "out"), 2)
  expect_equal(max_degree(g, loops = FALSE), 2)
  expect_equal(max_degree(g, mode = "out", loops = FALSE), 1)
  expect_equal(max_degree(g, mode = "in", loops = FALSE), 1)
  expect_equal(max_degree(g, v = c()), 0)
  expect_equal(max_degree(make_empty_graph()), 0)
})

test_that("mean_degree() works", {
  # Undirected graph: formula is 2 * edges / vertices
  g_undirected <- make_ring(10)
  expect_equal(mean_degree(g_undirected), 2)

  # Directed graph: formula is edges / vertices
  # This graph has edges 1->2, 2->2 (loop), 2->3, so 3 edges / 3 vertices = 1
  g_directed <- make_graph(c(1, 2, 2, 2, 2, 3), directed = TRUE)
  expect_equal(mean_degree(g_directed), 1)

  # Test loops parameter: excluding the self-loop (2->2)
  # Without loops: (3 edges - 1 loop) / 3 vertices = 2/3
  expect_equal(mean_degree(g_directed, loops = FALSE), 2 / 3)

  # Empty graph
  expect_true(is.nan(mean_degree(make_empty_graph())))

  # Compare with manual calculation for undirected graph
  g_random <- sample_gnp(100, 0.05)
  manual_mean <- 2 * ecount(g_random) / vcount(g_random)
  expect_equal(mean_degree(g_random), manual_mean)

  # For directed graph
  g_directed_random <- sample_gnp(100, 0.05, directed = TRUE)
  manual_mean_dir <- ecount(g_directed_random) / vcount(g_directed_random)
  expect_equal(mean_degree(g_directed_random), manual_mean_dir)
})

test_that("BFS uses 1-based root vertex index", {
  g <- make_ring(3)
  expect_equal(bfs(g, root = 1)$root, 1)
})

test_that("BFS works from multiple root vertices", {
  g <- make_ring(10) %du% make_ring(10)

  expect_equal(
    as.vector(bfs(g, 1)$order),
    c(1, 2, 10, 3, 9, 4, 8, 5, 7, 6, 11, 12, 20, 13, 19, 14, 18, 15, 17, 16)
  )

  expect_equal(
    as.vector(bfs(g, 1, unreachable = FALSE)$order),
    c(1, 2, 10, 3, 9, 4, 8, 5, 7, 6)
  )

  expect_equal(
    as.vector(bfs(g, c(1, 12), unreachable = FALSE)$order),
    c(1, 2, 10, 3, 9, 4, 8, 5, 7, 6, 12, 11, 13, 20, 14, 19, 15, 18, 16, 17)
  )

  expect_equal(
    as.vector(bfs(g, c(12, 1, 15), unreachable = FALSE)$order),
    c(12, 11, 13, 20, 14, 19, 15, 18, 16, 17, 1, 2, 10, 3, 9, 4, 8, 5, 7, 6)
  )
})

test_that("BFS the restricted set is one indexed", {
  # https://github.com/igraph/rigraph/issues/133
  g <- graph_from_edgelist(matrix(c(1, 2, 2, 3), ncol = 2, byrow = TRUE))

  expect_equal(
    as.numeric(bfs(g, 1, restricted = c(1, 2), unreachable = FALSE)$order),
    c(1, 2)
  )
})

test_that("BFS callback works", {
  env <- new.env()
  env$history <- list()

  callback <- function(graph, data, extra) {
    env$history <- append(env$history, list(data))
    FALSE
  }

  g <- make_ring(5, directed = TRUE)
  bfs(g, root = 3, mode = "out", callback = callback)

  names <- c("vid", "pred", "succ", "rank", "dist")
  expect_equal(
    env$history,
    list(
      setNames(c(3, 0, 4, 1, 0), names),
      setNames(c(4, 3, 5, 2, 1), names),
      setNames(c(5, 4, 1, 3, 2), names),
      setNames(c(1, 5, 2, 4, 3), names),
      setNames(c(2, 1, 0, 5, 4), names)
    )
  )
})

test_that("BFS callback does not blow up when an invalid value is returned", {
  env <- new.env()
  env$history <- list()

  callback <- function(graph, data, extra) {
    env$history <- append(env$history, list(data))
    data
  }

  g <- make_ring(5, directed = TRUE)
  bfs(g, root = 3, mode = "out", callback = callback)

  # returned value is coerced to TRUE so it should terminate the search after
  # one step

  names <- c("vid", "pred", "succ", "rank", "dist")
  expect_equal(
    env$history,
    list(setNames(c(3, 0, 4, 1, 0), names))
  )
})

test_that("BFS callback does not blow up when an error is raised within the callback", {
  callback <- function(graph, data, extra) {
    cli::cli_abort("test")
    FALSE
  }

  g <- make_ring(5, directed = TRUE)
  expect_error(bfs(g, root = 3, mode = "out", callback = callback), "test")

  expect_true(TRUE)
})

test_that("BFS callback does not blow up when another igraph function is raised within the callback", {
  skip("nested igraph call handling not implemented yet")

  callback <- function(graph, data, extra) {
    neighbors(graph, 1)
    FALSE
  }

  g <- make_ring(5, directed = TRUE)
  bfs(g, root = 3, mode = "out", callback = callback)

  expect_true(TRUE)
})

test_that("bfs() works", {
  local_igraph_options(print.id = FALSE)

  expect_snapshot({
    g <- graph_from_literal(a -+ b -+ c, z -+ a, d)
    bfs(
      g,
      root = 2,
      mode = "out",
      unreachable = FALSE,
      order = TRUE,
      rank = TRUE,
      parent = TRUE,
      pred = TRUE,
      succ = TRUE,
      dist = TRUE
    )
  })
})

test_that("bfs() deprecated arguments", {
  g <- graph_from_literal(a -+ b -+ c, z -+ a, d)

  expect_snapshot_igraph_error({
    b <- bfs(
      g,
      root = 2,
      neimode = "out",
      unreachable = FALSE,
      order = TRUE,
      rank = TRUE,
      father = TRUE,
      pred = TRUE,
      succ = TRUE,
      dist = TRUE
    )
  })
})

test_that("bfs() does not pad order", {
  g <- make_star(3)
  expect_equal(as.numeric(bfs(g, root = 2, unreachable = FALSE)$order), c(2, 1))

  local_igraph_options(return.vs.es = FALSE)
  expect_equal(as.numeric(bfs(g, root = 2, unreachable = FALSE)$order), c(2, 1))
})

test_that("diameter() works -- undirected", {
  g <- largest_component(sample_gnp(30, 3 / 30))
  sp <- distances(g)
  expect_equal(max(sp), diameter(g))

  g <- largest_component(sample_gnp(100, 1 / 100))
  sp <- distances(g)
  sp[sp == Inf] <- NA
  expect_equal(max(sp, na.rm = TRUE), diameter(g))
})

test_that("diameter() works -- directed", {
  g <- sample_gnp(30, 3 / 30, directed = TRUE)
  sp <- distances(g, mode = "out")
  sp[sp == Inf] <- NA
  expect_equal(max(sp, na.rm = TRUE), diameter(g, unconnected = TRUE))
})

test_that("diameter() works -- weighted", {
  g <- sample_gnp(30, 3 / 30, directed = TRUE)
  E(g)$weight <- sample(1:10, ecount(g), replace = TRUE)
  sp <- distances(g, mode = "out")
  sp[sp == Inf] <- NA
  expect_equal(max(sp, na.rm = TRUE), diameter(g, unconnected = TRUE))
})

test_that("diameter() works -- Bug #680538", {
  g <- make_tree(30, mode = "undirected")
  E(g)$weight <- 2
  expect_equal(diameter(g, unconnected = FALSE), 16)
})

test_that("diameter() correctly handles disconnected graphs", {
  g <- make_tree(7, 2, mode = "undirected") %du%
    make_tree(4, 3, mode = "undirected")
  expect_equal(diameter(g, unconnected = TRUE), 4)
  expect_equal(diameter(g, unconnected = FALSE), Inf)
  E(g)$weight <- 2
  expect_equal(diameter(g, unconnected = FALSE), Inf)
})

test_that("get_diameter() works", {
  g <- make_ring(10)
  E(g)$weight <- sample(seq_len(ecount(g)))
  d <- diameter(g)
  gd <- get_diameter(g)
  sp <- distances(g)

  expect_equal(d, max(sp))
  expect_equal(sp[gd[1], gd[length(gd)]], d)

  d <- diameter(g, weights = NA)
  gd <- get_diameter(g, weights = NA)
  sp <- distances(g, weights = NA)

  expect_equal(d, max(sp))
  length(gd) == d + 1
  expect_equal(sp[gd[1], gd[length(gd)]], d)
})

test_that("farthest_vertices() works", {
  kite <- graph_from_literal(
    Andre - Beverly:Carol:Diane:Fernando,
    Beverly - Andre:Diane:Ed:Garth,
    Carol - Andre:Diane:Fernando,
    Diane - Andre:Beverly:Carol:Ed:Fernando:Garth,
    Ed - Beverly:Diane:Garth,
    Fernando - Andre:Carol:Diane:Garth:Heather,
    Garth - Beverly:Diane:Ed:Fernando:Heather,
    Heather - Fernando:Garth:Ike,
    Ike - Heather:Jane,
    Jane - Ike
  )

  fn <- farthest_vertices(kite)
  fn$vertices <- as.vector(fn$vertices)
  expect_equal(fn, list(vertices = c(1, 10), distance = 4))

  expect_equal(
    distances(kite, v = fn$vertices[1], to = fn$vertices[2])[1],
    fn$distance
  )
  expect_equal(diameter(kite), fn$distance)
})

test_that("distances() works", {
  g <- make_graph(
    c(1, 5, 1, 7, 1, 8, 1, 10, 2, 6, 2, 7, 2, 8, 2, 10, 3, 4, 3, 5, 3, 9, 5, 6, 5, 7, 5, 10, 6, 8, 7, 8, 7, 9, 8, 9, 8, 10, 9, 10),
    directed = FALSE
  )

  mu <- distances(g, algorithm = "unweighted")

  # unit weights
  E(g)$weight <- rep(1, ecount(g))

  ma <- distances(g) # automatic
  md <- distances(g, algorithm = "dijkstra")
  mbf <- distances(g, algorithm = "bellman-ford")
  mj <- distances(g, algorithm = "johnson")
  mfw <- distances(g, algorithm = "floyd-warshall")

  expect_equal(mu, ma)
  expect_equal(mu, md)
  expect_equal(mu, mbf)
  expect_equal(mu, mj)
  expect_equal(mu, mfw)

  E(g)$weight <- 0.25 * (1:ecount(g))

  ma <- distances(g) # automatic
  md <- distances(g, algorithm = "dijkstra")
  mbf <- distances(g, algorithm = "bellman-ford")
  mj <- distances(g, algorithm = "johnson")
  mfw <- distances(g, algorithm = "floyd-warshall")

  expect_equal(ma, md)
  expect_equal(ma, mbf)
  expect_equal(ma, mj)
  expect_equal(ma, mfw)
})


test_that("all_shortest_paths() works", {
  edges <- matrix(
    c(
      "s", "a", 2, "s", "b", 4, "a", "t", 4, "b",
      "t", 2, "a", "1", 1, "a", "2", 1, "a", "3",
      2, "1", "b", 1, "2", "b", 2, "3", "b", 1
    ),
    byrow = TRUE,
    ncol = 3,
    dimnames = list(NULL, c("from", "to", "weight"))
  )
  edges <- as.data.frame(edges)
  edges[[3]] <- as.numeric(as.character(edges[[3]]))

  g <- graph_from_data_frame(as.data.frame(edges))

  sortlist <- function(list) {
    list <- lapply(list, sort)
    list <- lapply(list, as.vector)
    list[order(sapply(list, paste, collapse = "!"))]
  }

  sp1 <- all_shortest_paths(g, "s", "t", weights = NA)

  expect_equal(
    sortlist(sp1$vpaths),
    list(c(1, 2, 7), c(1, 3, 7))
  )
  expect_equal(
    sp1$nrgeo,
    c(1, 1, 1, 1, 1, 1, 2)
  )

  sp2 <- all_shortest_paths(g, "s", "t")

  expect_equal(
    sortlist(sp2$vpaths),
    list(c(1, 2, 3, 4, 7), c(1, 2, 7), c(1, 3, 7))
  )
  expect_equal(sp2$nrgeo, c(1, 1, 2, 1, 1, 1, 3))
})

test_that("shortest_paths() works", {
  edges <- matrix(
    c(
      "s", "a", 2, "s", "b", 4, "a", "t", 4,
      "b", "t", 2, "a", "1", 1, "a", "2", 1,
      "a", "3", 2, "1", "b", 1, "2", "b", 2,
      "3", "b", 1
    ),
    byrow = TRUE,
    ncol = 3,
    dimnames = list(NULL, c("from", "to", "weight"))
  )
  edges <- as.data.frame(edges)
  edges[[3]] <- as.numeric(as.character(edges[[3]]))

  g <- graph_from_data_frame(as.data.frame(edges))

  all1 <- all_shortest_paths(g, "s", "t", weights = NA)$vpaths

  s1 <- shortest_paths(g, "s", "t", weights = NA)

  expect_true(s1$vpath %in% all1)
})

test_that("shortest_paths() can handle negative weights", {
  g <- make_tree(7)
  E(g)$weight <- -1
  sps <- shortest_paths(g, 2)$vpath

  expect_true(length(sps) == 7)
  expect_equal(ignore_attr = TRUE, as.vector(sps[[1]]), integer(0))
  expect_equal(ignore_attr = TRUE, as.vector(sps[[2]]), c(2))
  expect_equal(ignore_attr = TRUE, as.vector(sps[[3]]), integer(0))
  expect_equal(ignore_attr = TRUE, as.vector(sps[[4]]), c(2, 4))
  expect_equal(ignore_attr = TRUE, as.vector(sps[[5]]), c(2, 5))
  expect_equal(ignore_attr = TRUE, as.vector(sps[[6]]), integer(0))
  expect_equal(ignore_attr = TRUE, as.vector(sps[[7]]), integer(0))
})

test_that("k_shortest_paths() works", {
  g <- make_ring(5)
  res <- k_shortest_paths(g, 1, 2, k = 3)
  expect_length(res$vpaths, 2)
  expect_length(res$epaths, 2)
  expect_equal(as.numeric(res$vpaths[[1]]), c(1, 2))
  expect_equal(as.numeric(res$epaths[[1]]), c(1))
  expect_equal(as.numeric(res$vpaths[[2]]), c(1, 5, 4, 3, 2))
  expect_equal(as.numeric(res$epaths[[2]]), c(5, 4, 3, 2))
})

test_that("k_shortest_paths() works with weights", {
  g <- make_graph(c(1, 2, 1, 3, 3, 2))
  E(g)$weight <- c(5, 2, 1)
  res <- k_shortest_paths(g, 1, 2, k = 3)
  expect_length(res$vpaths, 2)
  expect_length(res$epaths, 2)
  expect_equal(as.numeric(res$vpaths[[1]]), c(1, 3, 2))
  expect_equal(as.numeric(res$epaths[[1]]), c(2, 3))
  expect_equal(as.numeric(res$vpaths[[2]]), c(1, 2))
  expect_equal(as.numeric(res$epaths[[2]]), c(1))
})

test_that("transitivity() works", {
  withr::local_seed(42)
  g <- sample_gnp(100, p = 10 / 100)

  t1 <- transitivity(g, type = "global")
  expect_equal(t1, 0.10483870967741935887)

  t2 <- transitivity(g, type = "average")
  expect_equal(t2, 0.10159943848720931481)

  t3 <- transitivity(g, type = "local", vids = V(g))
  t33 <- transitivity(g, type = "local")
  est3 <- structure(
    c(0, 0.06667, 0.1028, 0.1016, 0.1333, 0.2222),
    .Names = c(
      "Min.",
      "1st Qu.",
      "Median",
      "Mean",
      "3rd Qu.",
      "Max."
    ),
    class = c("summaryDefault", "table")
  )
  expect_equal(summary(t3), est3, tolerance = 1e-3)
  expect_equal(summary(t33), est3, tolerance = 1e-3)
})

test_that("no integer overflow", {
  withr::local_seed(42)
  g <- make_star(80000, mode = "undirected") + edges(sample(2:1000), 100)
  mtr <- min(transitivity(g, type = "local"), na.rm = TRUE)
  expect_true(mtr > 0)
})

# Check that transitivity() produces named vectors, see #943
# The four tests below check four existing code paths
test_that("local transitivity() produces named vectors", {
  g <- make_graph(~ a - b - c - a - d)
  E(g)$weight <- 1:4
  t1 <- transitivity(g, type = "local")
  expect_equal(names(t1), V(g)$name)

  t2 <- transitivity(g, type = "barrat")
  expect_equal(names(t2), V(g)$name)

  vs <- c("a", "c")
  t3 <- transitivity(g, type = "local", vids = vs)
  expect_equal(names(t3), vs)

  t4 <- transitivity(g, type = "barrat", vids = vs)
  expect_equal(names(t4), vs)
})

test_that("constraint() works", {
  constraint.orig <- function(graph, nodes = V(graph), attr = NULL) {
    ensure_igraph(graph)
    idx <- degree(graph) != 0
    A <- as_adjacency_matrix(graph, attr = attr, sparse = FALSE)
    A <- A[idx, idx]
    n <- sum(idx)

    one <- c(rep(1, n))
    CZ <- A + t(A)
    cs <- CZ %*% one # degree of vertices
    ics <- 1 / cs
    CS <- ics %*% t(one) # 1/degree of vertices
    P <- CZ * CS # intermediate result: proportionate tie strengths
    PSQ <- P %*% P # sum paths of length two
    P.bi <- as.numeric(P > 0) # exclude paths to non-contacts (& reflexive):
    PC <- (P + (PSQ * P.bi))^2 # dyadic constraint
    ci <- PC %*% one # overall constraint
    dim(ci) <- NULL

    ci2 <- numeric(vcount(graph))
    ci2[idx] <- ci
    ci2[!idx] <- NaN
    ci2[nodes]
  }

  karate <- make_graph("Zachary")

  c1 <- constraint(karate)
  c2 <- constraint.orig(karate)
  expect_equal(c1, c2)

  withr::local_seed(42)
  E(karate)$weight <- sample(1:10, replace = TRUE, ecount(karate))
  wc1 <- constraint(karate)
  wc2 <- constraint.orig(karate, attr = "weight")
  expect_equal(wc1, wc2)
})

test_that("ego() works", {
  neig <- function(graph, order, vertices) {
    sp <- distances(graph)
    v <- unique(unlist(lapply(vertices, function(x) {
      w <- which(sp[x, ] <= order)
    })))
    induced_subgraph(graph, c(v, vertices))
  }

  g <- sample_gnp(50, 5 / 50)

  v <- sample(vcount(g), 1)
  g1 <- make_ego_graph(g, 2, v)[[1]]
  g2 <- neig(g, 2, v)
  expect_isomorphic(g1, g2)

  #########

  nei <- function(graph, order, vertices) {
    sp <- distances(graph)
    v <- unique(unlist(lapply(vertices, function(x) {
      w <- which(sp[x, ] <= order)
    })))
    v
  }

  v1 <- ego(g, 2, v)[[1]]
  v2 <- nei(g, 2, v)
  expect_equal(as.vector(sort(v1)), sort(v2))

  #########

  s <- ego_size(g, 2, v)[[1]]
  expect_equal(s, length(v1))
})

test_that("mindist works", {
  g <- make_ring(10)
  expect_equal(ego_size(g, order = 2, mindist = 0), rep(5, 10))
  expect_equal(ego_size(g, order = 2, mindist = 1), rep(4, 10))
  expect_equal(ego_size(g, order = 2, mindist = 2), rep(2, 10))

  n0 <- unvs(ego(g, order = 2, 5:6, mindist = 0))
  n1 <- unvs(ego(g, order = 2, 5:6, mindist = 1))
  n2 <- unvs(ego(g, order = 2, 5:6, mindist = 2))

  expect_equal(lapply(n0, sort), list(3:7, 4:8))
  expect_equal(lapply(n1, sort), list(c(3, 4, 6, 7), c(4, 5, 7, 8)))
  expect_equal(lapply(n2, sort), list(c(3, 7), c(4, 8)))

  ng0 <- make_ego_graph(g, order = 2, 5:6, mindist = 0)
  ng1 <- make_ego_graph(g, order = 2, 5:6, mindist = 1)
  ng2 <- make_ego_graph(g, order = 2, 5:6, mindist = 2)

  expect_equal(sapply(ng0, vcount), c(5, 5))
  expect_equal(sapply(ng1, vcount), c(4, 4))
  expect_equal(sapply(ng2, vcount), c(2, 2))

  expect_equal(sapply(ng0, ecount), c(4, 4))
  expect_equal(sapply(ng1, ecount), c(2, 2))
  expect_equal(sapply(ng2, ecount), c(0, 0))
})

test_that("is_matching() works", {
  df <- data.frame(x = 1:5, y = letters[1:5])
  g <- graph_from_data_frame(df)

  expect_true(is_matching(g, c(6:10, 1:5)))
  expect_true(is_matching(g, c(6:9, NA, 1:4, NA)))
  expect_true(is_matching(g, rep(NA, 10)))

  expect_false(is_matching(g, c(1:10)))
  expect_false(is_matching(g, c(6:10, 5:1)))
  expect_false(is_matching(g, c(2)))
})

test_that("is_matching() works with names", {
  df <- data.frame(x = 1:5, y = letters[1:5])
  g <- graph_from_data_frame(df)

  expect_true(is_matching(
    g,
    c("a", "b", "c", "d", "e", "1", "2", "3", "4", "5")
  ))
  expect_true(is_matching(g, c("a", "b", "c", "d", NA, "1", "2", "3", "4", NA)))

  expect_false(is_matching(
    g,
    c("1", "2", "3", "4", "5", "a", "b", "c", "d", "e")
  ))
  expect_false(is_matching(
    g,
    c("a", "b", "c", "d", "e", "5", "4", "3", "2", "1")
  ))
  expect_false(is_matching(g, c("a", "b")))
})

test_that("is_max_matching() works", {
  df <- data.frame(x = 1:5, y = letters[1:5])
  g <- graph_from_data_frame(df)

  expect_true(is_max_matching(g, c(6:10, 1:5)))
  expect_false(is_max_matching(g, c(6:9, NA, 1:4, NA)))
  expect_false(is_max_matching(g, rep(NA, 10)))

  expect_false(is_max_matching(g, c(1:10)))
  expect_false(is_max_matching(g, c(6:10, 5:1)))
  expect_false(is_max_matching(g, c(2)))
})

test_that("is_max_matching() works with names", {
  df <- data.frame(x = 1:5, y = letters[1:5])
  g <- graph_from_data_frame(df)

  expect_true(is_max_matching(
    g,
    c("a", "b", "c", "d", "e", "1", "2", "3", "4", "5")
  ))
  expect_false(is_max_matching(
    g,
    c("a", "b", "c", "d", NA, "1", "2", "3", "4", NA)
  ))

  expect_false(is_max_matching(
    g,
    c("1", "2", "3", "4", "5", "a", "b", "c", "d", "e")
  ))
  expect_false(is_max_matching(
    g,
    c("a", "b", "c", "d", "e", "5", "4", "3", "2", "1")
  ))
  expect_false(is_max_matching(g, c("a", "b")))
})

test_that("max_bipartite_match() works", {
  df <- data.frame(x = 1:5, y = letters[1:5])
  g <- graph_from_data_frame(df)
  V(g)$type <- 1:vcount(g) > 5
  match <- max_bipartite_match(g)

  expect_equal(match$matching_size, 5)
  expect_equal(match$matching_weight, 5)
  expect_equal(sort(as.vector(match$matching)), sort(V(g)$name))
})

test_that("max_bipartite_match() handles missing types gracefully", {
  df <- data.frame(x = 1:5, y = letters[1:5])
  g <- graph_from_data_frame(df)
  expect_error(max_bipartite_match(g), "supply .*types.* argument")
})

test_that("unfold_tree() works", {
  g <- make_tree(7, 2)
  g <- add_edges(g, c(2, 7, 1, 4))
  g2 <- unfold_tree(g, roots = 1)
  expect_isomorphic(
    g2$tree,
    make_graph(c(
      1, 2, 1, 3, 2, 8, 2, 5, 3, 6, 3, 9, 2, 7, 1, 4
    ))
  )
  expect_equal(g2$vertex_index, c(1, 2, 3, 4, 5, 6, 7, 4, 7))
})

test_that("count_components() counts correctly", {
  g <- make_star(20, "undirected")
  h <- make_ring(10)

  G <- disjoint_union(g, h)

  expect_equal(count_components(G), 2L)
})

test_that("a null graph has zero components", {
  g <- make_empty_graph(0)

  expect_equal(count_components(g), 0L)
})

test_that("count_reachable() works for directed graphs", {
  # Directed path graph: 1 -> 2 -> 3 -> 4 -> 5
  g <- make_graph(~ 1 -+ 2 -+ 3 -+ 4 -+ 5)

  # Out mode: count vertices reachable via outgoing edges
  out_counts <- count_reachable(g, mode = "out")
  expect_equal(out_counts, c(5, 4, 3, 2, 1))

  # In mode: count vertices from which current vertex is reachable
  in_counts <- count_reachable(g, mode = "in")
  expect_equal(in_counts, c(1, 2, 3, 4, 5))

  # All mode: treat as undirected
  all_counts <- count_reachable(g, mode = "all")
  expect_equal(all_counts, rep(5, 5))
})

test_that("count_reachable() works for undirected graphs", {
  # Undirected path graph
  g <- make_graph(~ 1 - 2 - 3 - 4 - 5)

  # All vertices are reachable from each other in an undirected path
  counts <- count_reachable(g, mode = "out")
  expect_equal(counts, rep(5, 5))
})

test_that("count_reachable() works for graphs with multiple components", {
  # Graph with three components: 1-2-3, 4-5, 6
  g <- make_graph(~ 1 - 2 - 3, 4 - 5, 6)

  counts <- count_reachable(g, mode = "all")
  expect_equal(counts, c(3, 3, 3, 2, 2, 1))
})

test_that("count_reachable() works for empty and single-vertex graphs", {
  # Empty graph
  g0 <- make_empty_graph(0)
  expect_equal(count_reachable(g0, mode = "out"), integer(0))

  # Single vertex
  g1 <- make_empty_graph(1)
  expect_equal(count_reachable(g1, mode = "out"), 1L)
})

test_that("girth() works", {
  ## No circle in a tree
  g <- make_tree(1000, 3)
  gi <- girth(g)
  expect_equal(gi$girth, Inf)
  expect_equal(as.vector(gi$circle), numeric())

  ## The worst case running time is for a ring
  g <- make_ring(100)
  gi <- girth(g)
  expect_equal(gi$girth, 100)
  expect_equal(sort(diff(as.vector(gi$circle))), c(-99, rep(1, 98)))
})

test_that("coreness() works", {
  g <- make_ring(10)
  g <- add_edges(g, c(1, 2, 2, 3, 1, 3))
  gc <- coreness(g)
  expect_equal(gc, c(3, 3, 3, 2, 2, 2, 2, 2, 2, 2))
})

test_that("laplacian_matrix() works", {
  skip_if_not_installed("Matrix")
  mat <- rbind(
    c(116, 210, 200),
    c(210, 386, 380),
    c(200, 380, 401)
  )

  Ai <- graph_from_adjacency_matrix(
    as(mat, "dgCMatrix"),
    weighted = TRUE,
    mode = "undirected"
  )

  expect_snapshot(laplacian_matrix(Ai, normalization = "unnormalized"))
})

test_that("mean_distance works", {
  avg_path_length <- function(graph) {
    sp <- distances(graph, mode = "out")
    if (is_directed(graph)) {
      diag(sp) <- NA
    } else {
      sp[lower.tri(sp, diag = TRUE)] <- NA
    }
    sp[sp == "Inf"] <- NA
    mean(sp, na.rm = TRUE)
  }

  giant.component <- function(graph, mode = "weak") {
    clu <- components(graph, mode = mode)
    induced_subgraph(graph, which(clu$membership == which.max(clu$csize)))
  }

  g <- giant.component(sample_gnp(100, 3 / 100))
  expect_equal(avg_path_length(g), mean_distance(g))

  g <- giant.component(
    sample_gnp(100, 6 / 100, directed = TRUE),
    mode = "strong"
  )
  expect_equal(avg_path_length(g), mean_distance(g))

  g <- sample_gnp(100, 2 / 100)
  expect_equal(avg_path_length(g), mean_distance(g))

  g <- sample_gnp(100, 4 / 100, directed = TRUE)
  expect_equal(avg_path_length(g), mean_distance(g))
})

test_that("mean_distance works correctly for disconnected graphs", {
  g <- make_full_graph(5) %du% make_full_graph(7)
  md <- mean_distance(g, unconnected = FALSE)
  expect_equal(Inf, md)
  md <- mean_distance(g, unconnected = TRUE)
  expect_equal(1, md)
})

test_that("mean_distance can provide details", {
  avg_path_length <- function(graph) {
    sp <- distances(graph, mode = "out")
    if (is_directed(graph)) {
      diag(sp) <- NA
    } else {
      sp[lower.tri(sp, diag = TRUE)] <- NA
    }
    sp[sp == "Inf"] <- NA
    mean(sp, na.rm = TRUE)
  }

  giant.component <- function(graph, mode = "weak") {
    clu <- components(graph, mode = mode)
    induced_subgraph(graph, which(clu$membership == which.max(clu$csize)))
  }

  g <- giant.component(sample_gnp(100, 3 / 100))
  md <- mean_distance(g, details = TRUE)
  expect_equal(avg_path_length(g), md$res)

  g <- make_full_graph(5) %du% make_full_graph(7)
  md <- mean_distance(g, details = TRUE, unconnected = TRUE)
  expect_equal(1, md$res)
  expect_equal(70, md$unconnected)

  g <- make_full_graph(5) %du% make_full_graph(7)
  md <- mean_distance(g, details = TRUE, unconnected = FALSE)
  expect_equal(Inf, md$res)
  expect_equal(70, md$unconnected)
})

test_that("any_multiple(), count_multiple(), which_multiple() works", {
  # g <- sample_pa(10, m = 3, algorithm = "bag")
  g <- graph_from_edgelist(cbind(
    c(2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10),
    c(1, 1, 1, 1, 1, 1, 1, 2, 3, 4, 3, 4, 3, 1, 1, 1, 3, 1, 2, 4, 1, 1, 2, 4, 1, 4, 1)
  ))
  im <- which_multiple(g)
  cm <- count_multiple(g)
  expect_true(any_multiple(g))
  expect_equal(
    im,
    c(
      FALSE, TRUE, TRUE, FALSE, TRUE, TRUE, FALSE, FALSE, FALSE,
      FALSE, FALSE, TRUE, FALSE, FALSE, TRUE, FALSE, FALSE, TRUE,
      FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, TRUE
    )
  )
  expect_equal(
    cm,
    c(3, 3, 3, 3, 3, 3, 1, 1, 1, 2, 1, 2, 1, 2, 2, 2, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 2)
  )
  expect_equal(
    count_multiple(simplify(g)),
    rep(1, ecount(simplify(g)))
  )

  ## Direction of the edge is important
  expect_false(any_multiple(make_graph(c(1, 2, 2, 1))))
  expect_equal(which_multiple(make_graph(c(1, 2, 2, 1))), c(FALSE, FALSE))
  expect_equal(
    which_multiple(make_graph(c(1, 2, 2, 1), dir = FALSE)),
    c(FALSE, TRUE)
  )

  ## Remove multiple edges but keep multiplicity
  # g <- sample_pa(10, m = 3, algorithm = "bag")
  g <- graph_from_edgelist(cbind(
    c(2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10),
    c(1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 4, 1, 4, 1, 1, 6, 4, 1, 5, 8)
  ))
  E(g)$weight <- 1
  g <- simplify(g)
  expect_false(any_multiple(g))
  expect_false(any(which_multiple(g)))
  expect_equal(
    E(g)$weight,
    c(3, 2, 1, 2, 1, 3, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1)
  )
})

test_that("any_loop(), which_loop(), count_loops() works", {
  # Graph with loops
  g <- make_graph(c(1, 1, 2, 2, 3, 3, 4, 5))
  expect_true(any_loop(g))
  expect_equal(which_loop(g), c(TRUE, TRUE, TRUE, FALSE))
  expect_equal(count_loops(g), 3)

  # Graph without loops
  g2 <- make_graph(c(1, 2, 2, 3, 3, 4))
  expect_false(any_loop(g2))
  expect_equal(which_loop(g2), c(FALSE, FALSE, FALSE))
  expect_equal(count_loops(g2), 0)

  # Empty graph
  g3 <- make_empty_graph(n = 5)
  expect_false(any_loop(g3))
  expect_equal(count_loops(g3), 0)

  # Graph with only loops
  g4 <- make_graph(c(1, 1, 2, 2, 3, 3))
  expect_true(any_loop(g4))
  expect_equal(which_loop(g4), c(TRUE, TRUE, TRUE))
  expect_equal(count_loops(g4), 3)
})

test_that("edge_density works", {
  g <- sample_gnp(50, 4 / 50)
  gd <- edge_density(g)
  gd2 <- ecount(g) / vcount(g) / (vcount(g) - 1) * 2
  expect_equal(gd, gd2)

  ####

  g <- sample_gnp(50, 4 / 50, directed = TRUE)
  gd <- edge_density(g)
  gd2 <- ecount(g) / vcount(g) / (vcount(g) - 1)
  expect_equal(gd, gd2)
})

test_that("knn works", {
  withr::local_seed(42)

  ## Some trivial ones
  g <- make_ring(10)
  expect_equal(knn(g), list(knn = rep(2, 10), knnk = c(NaN, 2)))

  g2 <- make_star(10)
  expect_equal(
    knn(g2),
    list(knn = c(1, rep(9, 9)), knnk = c(9, rep(NaN, 7), 1))
  )

  ## A scale-free one, try to plot 'knnk'
  g3 <- simplify(sample_pa(1000, m = 5))
  r3 <- knn(g3)
  expect_equal(r3$knn[43], 46)
  expect_equal(r3$knn[1000], 192.4)
  expect_equal(r3$knnk[100], 18.78)
  expect_equal(length(r3$knnk), 359)

  ## A random graph
  g4 <- sample_gnp(1000, p = 5 / 1000)
  r4 <- knn(g4)
  expect_equal(r4$knn[1000], 20 / 3)
  expect_equal(length(r4$knnk), 15)
  expect_equal(r4$knnk[12], 19 / 3)

  ## A weighted graph
  g5 <- make_star(10)
  E(g5)$weight <- seq(ecount(g5))
  r5 <- knn(g5)
  expect_equal(
    r5,
    structure(
      list(
        knn = c(1, rep(9, 9)),
        knnk = c(9, NaN, NaN, NaN, NaN, NaN, NaN, NaN, 1)
      ),
      .Names = c("knn", "knnk")
    )
  )
})

test_that("reciprocity works", {
  g <- make_graph(c(1, 2, 2, 1, 2, 3, 3, 4, 4, 4), directed = TRUE)
  expect_equal(reciprocity(g), 0.5)
  expect_equal(reciprocity(g, ignore.loops = FALSE), 0.6)
})

test_that("feedback_arc_set works", {
  skip_if_no_glpk()

  g <- make_graph(c(1, 2, 2, 3, 3, 1, 4, 2, 3, 4), directed = TRUE)
  fas <- feedback_arc_set(g)
  expect_equal(as.vector(fas), c(2))
})

test_that("feedback_arc_set works with weights", {
  skip_if_no_glpk()

  g <- make_ring(4, directed = TRUE)
  E(g)$weight <- 4:1
  fas <- feedback_arc_set(g)
  expect_equal(as.vector(fas), c(4))
})

test_that("feedback_arc_set works with undirected graphs", {
  skip_if_no_glpk()

  g <- make_ring(10)
  fas <- feedback_arc_set(g)
  expect_length(fas, 1)
})

test_that("feedback_vertex_set works", {
  skip_if_no_glpk()

  g <- make_lattice(c(3, 3))
  fvs <- feedback_vertex_set(g)
  expect_length(fvs, 2)
})

test_that("feedback_vertex_set works with weights", {
  skip_if_no_glpk()

  g <- make_ring(5, directed = TRUE)
  V(g)$weight <- 5:1
  fvs <- feedback_vertex_set(g)
  expect_equal(as.vector(fvs), c(5))
})
