test_that("init_motif_specs accepts built-in names and produces correct structure", {
  specs <- init_motif_specs(list("v_shape", "triangle", "star3"))

  expect_equal(specs$names, c("v_shape", "triangle", "star3"))
  expect_equal(specs$edge_counts, c(2L, 3L, 3L))
  expect_true(is.function(specs$compute))
})

test_that("init_motif_specs rejects unknown built-in names", {
  expect_error(
    init_motif_specs(list("pentagon")),
    regexp = "Unknown built-in motif"
  )
})

test_that("init_motif_specs accepts a user-supplied spec returning one value", {
  my_spec <- list(
    fn      = function(A) c(my_edge = sum(A) / 2),
    n_edges = 1L
  )
  specs <- init_motif_specs(list(my_spec))

  expect_equal(specs$names, "my_edge")
  expect_equal(specs$edge_counts, 1L)
  A <- make_adj(10, 0.3, seed = 1)
  out <- specs$compute(A)
  expect_equal(out[["my_edge"]], sum(A) / 2)
})

test_that("init_motif_specs accepts a user-supplied spec returning two values", {
  my_spec <- list(
    fn      = function(A) c(motif_a = sum(A) / 2, motif_b = nrow(A)),
    n_edges = c(1L, 0L)
  )
  specs <- init_motif_specs(list("v_shape", my_spec))

  expect_equal(specs$names, c("v_shape", "motif_a", "motif_b"))
  expect_equal(length(specs$edge_counts), 3L)
})

test_that("init_motif_specs errors when fn output length mismatches n_edges", {
  bad_spec <- list(
    fn      = function(A) c(a = 1, b = 2),
    n_edges = 3L  # wrong length
  )
  expect_error(
    init_motif_specs(list(bad_spec)),
    regexp = "Length of 'n_edges'"
  )
})

test_that("init_motif_specs errors when fn is not a function", {
  bad_spec <- list(fn = 42, n_edges = 1L)
  expect_error(init_motif_specs(list(bad_spec)), regexp = "must be a function")
})

# ------------------------------------------------------------------

test_that("compute_network_moments returns correct density", {
  A     <- make_adj(10, 0.4, seed = 5)
  specs <- init_motif_specs(list("v_shape"))
  out   <- compute_network_moments(A, specs)

  expected_rho <- sum(A) / (10 * 9)
  expect_equal(out$rho, expected_rho)
})

test_that("compute_network_moments returns zeros for empty graph", {
  A     <- matrix(0L, 8, 8)
  specs <- init_motif_specs(list("v_shape", "triangle", "star3"))
  out   <- compute_network_moments(A, specs)

  expect_equal(out$rho, 0)
  expect_true(all(out$UR == 0))
})

test_that("compute_network_moments returns zeros when k < 3", {
  A     <- make_adj(2, 0.5, seed = 1)
  specs <- init_motif_specs(list("v_shape", "triangle"))
  out   <- compute_network_moments(A, specs)

  expect_equal(out$rho, 0)
  expect_true(all(out$UR == 0))
})

test_that("compute_network_moments v_shape moment is non-negative", {
  A     <- make_adj(20, 0.3, seed = 7)
  specs <- init_motif_specs(list("v_shape"))
  out   <- compute_network_moments(A, specs)
  expect_true(out$UR[["v_shape"]] >= 0)
})

# ------------------------------------------------------------------

test_that("sparsify_graph returns matrix of same dimensions", {
  A    <- make_adj(15, 0.5, seed = 2)
  out  <- sparsify_graph(A, 0.5)
  expect_equal(dim(out), dim(A))
})

test_that("sparsify_graph with p=1 is identity", {
  A   <- make_adj(10, 0.4, seed = 3)
  out <- sparsify_graph(A, 1)
  expect_identical(out, A)
})

test_that("sparsify_graph with p=0 gives zero matrix", {
  A   <- make_adj(10, 0.4, seed = 4)
  out <- sparsify_graph(A, 0)
  expect_true(all(out == 0L))
})

test_that("sparsify_graph result is symmetric", {
  set.seed(10)
  A   <- make_adj(12, 0.5, seed = 10)
  out <- sparsify_graph(A, 0.6)
  expect_equal(out, t(out))
})

test_that("sparsify_graph result is a subset of input edges", {
  A   <- make_adj(20, 0.6, seed = 11)
  out <- sparsify_graph(A, 0.4)
  # Every edge in out must also be in A.
  expect_true(all(out[A == 0L] == 0L))
})

# ------------------------------------------------------------------

test_that("subsample_moments returns correct structure", {
  A   <- make_adj(80, 0.15, seed = 20)
  res <- subsample_moments(A, b = 20, N_sub = 30)

  expect_named(res, c("rho", "moments", "motif_names", "b"))
  expect_equal(nrow(res$moments), 30L)
  expect_equal(ncol(res$moments), 3L)
  expect_equal(res$motif_names, c("v_shape", "triangle", "star3"))
  expect_equal(res$b, 20L)
})

test_that("subsample_moments density estimate is in (0,1)", {
  A   <- make_adj(60, 0.2, seed = 21)
  res <- subsample_moments(A, b = 15, N_sub = 20)
  expect_true(res$rho > 0 && res$rho < 1)
})

test_that("subsample_moments errors when b >= n", {
  A <- make_adj(20, 0.2, seed = 22)
  expect_error(subsample_moments(A, b = 20, N_sub = 10), regexp = "strictly less")
})

test_that("subsample_moments works with a single motif", {
  A   <- make_adj(50, 0.2, seed = 23)
  res <- subsample_moments(A, b = 15, N_sub = 20, motifs = list("triangle"))

  expect_equal(ncol(res$moments), 1L)
  expect_equal(colnames(res$moments), "triangle")
})

test_that("subsample_moments works with a user-supplied motif", {
  edge_count_fn <- list(
    fn      = function(A) c(n_edges = sum(A) / 2),
    n_edges = 1L
  )
  A   <- make_adj(50, 0.2, seed = 24)
  res <- subsample_moments(A, b = 15, N_sub = 20,
                           motifs = list("v_shape", edge_count_fn))

  expect_equal(ncol(res$moments), 2L)
  expect_equal(colnames(res$moments), c("v_shape", "n_edges"))
})

# ------------------------------------------------------------------

test_that("sparsify_moments returns named vector of correct length", {
  G1  <- make_adj(40, 0.20, seed = 30)
  G2  <- make_adj(40, 0.20, seed = 31)
  out <- sparsify_moments(G1, G2, rho_target = 0.08)

  expect_true(is.numeric(out))
  expect_equal(length(out), 3L)
  expect_equal(names(out), c("v_shape", "triangle", "star3"))
})

test_that("sparsify_moments returns NA for empty G1", {
  G1  <- matrix(0L, 20, 20)
  G2  <- make_adj(20, 0.3, seed = 32)
  out <- sparsify_moments(G1, G2, rho_target = 0.05)
  expect_true(all(is.na(out)))
})

test_that("sparsify_moments errors on invalid rho_target", {
  G1 <- make_adj(20, 0.2, seed = 33)
  G2 <- make_adj(20, 0.2, seed = 34)
  expect_error(sparsify_moments(G1, G2, rho_target = 1.5))
  expect_error(sparsify_moments(G1, G2, rho_target = 0))
})

# ------------------------------------------------------------------

test_that("two_sample_test returns correct structure (mahalanobis)", {
  set.seed(40)
  G       <- make_adj(200, 0.10)
  G_prime <- make_adj(60,  0.10)

  res <- two_sample_test(G, G_prime, rho_target = 0.06, N_sub = 100)

  expect_named(
    res,
    c("p_value", "test_statistic", "observed_moments", "null_moments",
      "null_statistic", "method", "motif_names")
  )
  expect_true(res$p_value >= 0 && res$p_value <= 1)
  expect_equal(res$method, "mahalanobis")
  expect_equal(res$motif_names, c("v_shape", "triangle", "star3"))
  expect_equal(nrow(res$null_moments), 100L)
})

test_that("two_sample_test returns correct structure (cauchy)", {
  set.seed(41)
  G       <- make_adj(200, 0.10)
  G_prime <- make_adj(60,  0.10)

  res <- two_sample_test(G, G_prime, rho_target = 0.06,
                         N_sub = 100, test = "cauchy")

  expect_equal(res$method, "cauchy")
  expect_named(res$marginal_p_values, c("v_shape", "triangle", "star3"))
  expect_true(res$p_value >= 0 && res$p_value <= 1)
})

test_that("two_sample_test p-value is near nominal level under null (rough check)", {
  # Generate 20 repetitions under the null; expect rejection rate near 0.05
  # for alpha = 0.05.  With 20 reps this is a very loose sanity check.
  set.seed(42)
  pvals <- replicate(20, {
    G       <- make_adj(150, 0.12)
    G_prime <- make_adj(50,  0.12)
    res     <- two_sample_test(G, G_prime, rho_target = 0.07,
                               N_sub = 200, test = "mahalanobis")
    res$p_value
  })
  # Under the null, p-values should be roughly uniform, so most > 0.05.
  expect_true(mean(pvals > 0.05) > 0.5)
})

test_that("two_sample_test errors when G_prime too small to split", {
  G       <- make_adj(50, 0.2, seed = 50)
  G_prime <- make_adj(4,  0.2, seed = 51)   # b_half = 2 < 3
  expect_error(
    two_sample_test(G, G_prime, rho_target = 0.1, N_sub = 50),
    regexp = "at least 6"
  )
})

test_that("two_sample_test errors on invalid rho_target", {
  G       <- make_adj(100, 0.1, seed = 52)
  G_prime <- make_adj(30,  0.1, seed = 53)
  expect_error(two_sample_test(G, G_prime, rho_target = 2,  N_sub = 50))
  expect_error(two_sample_test(G, G_prime, rho_target = -1, N_sub = 50))
})

test_that("two_sample_test validate_adjacency catches non-square input", {
  bad <- matrix(0L, 3, 4)
  G   <- make_adj(50, 0.1, seed = 54)
  expect_error(two_sample_test(bad, G, rho_target = 0.05, N_sub = 50))
})
