################################################################################
# Tests for the optmatch object and basic methods
################################################################################
context("Optmatch object")

test_that("Object creation", {
  dist <- diag(5)
  dimnames(dist) <- list(letters[1:5], letters[6:10])

  # recreate the result of running fullmatch. Must have err and cells fields
  ms <- list(
    list(err = 0, cells = c(a = 1, f = 1, b = 2, g = 2)),
    list(err = 0, cells = c(c = 1, h = 1, d = 2, i = 2, e = 3, j = 3)))

  res.opt <- makeOptmatch(dist, ms, NULL)

  expect_equal(length(res.opt), 10)
  expect_is(res.opt, "factor")
  expect_is(res.opt, "optmatch")

  # two levels of matches shouldn't be 1.NA, 2.NA, should just be NA
  ms2 <- list(
    list(err = 0, cells = c(a = 1, f = 1, b = 1, g = NA)),
    list(err = 0, cells = c(c = 1, h = 1, d = 2, i = 2, e = 2, j = NA)))

  res.opt2 <- makeOptmatch(dist, ms2, NULL)
  expect_true(all(is.na(res.opt2[c("g", "j")])))

})

test_that("Object subsetting", {
  dist <- diag(5)
  dimnames(dist) <- list(letters[1:5], letters[6:10])

  ms <- list(list(err = 0, cells = c(a = 1, f = 1, b = 2, g = 2)),
             list(err = 0, cells = c(c = 1, h = 1, d = 2, i = 2, e = 3, j = 3)))

  res.opt <- makeOptmatch(dist, ms, NULL)

  expect_equal(names(res.opt[1:4]), c("a", "f", "b", "g"))
  expect_equal(length(res.opt[c("a", "b")]), 2)

})

test_that("Subsetting preserves subproblem", {
  data(nuclearplants)

  # 1 subproblem
  f <- fullmatch(pr ~ cost, data=nuclearplants)

  ssf <- f[25:28]
  spssf <- attr(ssf, "subproblem")

  expect_true(all(spssf ==  attr(f, "subproblem")[25:28]))
  expect_true(all.equal(names(spssf),names(ssf)))


  # 2 subproblems
  f <- fullmatch(pr ~ cost, within=exactMatch(pr ~ pt, data=nuclearplants), data=nuclearplants)

  ssf <- f[25:28]
  spssf <- attr(ssf, "subproblem")

  expect_true(all(spssf ==  attr(f, "subproblem")[25:28]))
  expect_true(all.equal(names(spssf),names(ssf)))

  # no subproblems
  f <- fullmatch(pr ~ cost, data=nuclearplants)
  attr(f, "subproblem") <- NULL

  ssf <- f[25:28]
  spssf <- attr(ssf, "subproblem")

  expect_true(is.null(spssf))

})

test_that("Matched distances", {
  # see R/matched.distances.R for the function
  # it is only called by makeOptmatch internally, so putting the tests here
  # start with an easy case:
  dist <- matrix(Inf, nrow = 5, ncol = 5)
  diag(dist) <- 1:5
  dimnames(dist) <- list(letters[1:5], letters[6:10])
  dist.match <- as.factor(c(1.1,1.1,1.2,1.2,2.1,2.1,2.2,2.2,2.3,2.3))
  names(dist.match) <- c("a","f","b","g","c","h","d","i","e","j")
  class(dist.match) <- c("optmatch", "factor")

  res.md <- matched.distances(dist.match, dist)
  expect_equivalent(as.vector(res.md), 1:5)

  # now an ISM version
  dist.i <- as.InfinitySparseMatrix(dist)
  res.mdi <- matched.distances(dist.match, dist.i)
  expect_equivalent(as.vector(res.mdi), 1:5)

  # proper names
  res.names <- matched.distances(dist.match, dist, preserve.unit.names = TRUE)
  expect_equal(names(res.names), c("1.1", "1.2", "2.1", "2.2", "2.3"))

  res.names.i <- matched.distances(dist.match, dist.i, preserve.unit.names = TRUE)
  expect_equal(names(res.names.i), c("1.1", "1.2", "2.1", "2.2", "2.3"))

  # matches with more than one item in a strata
  match.multiple <- as.factor(c(1.1,1.1,NA,1.1,2.1,2.1,2.2,2.2,2.3,2.3))
  names(match.multiple) <- c("a","f","b","g","c","h","d","i","e","j")
  class(match.multiple) <- c("optmatch", "factor")

  dist.multiple <- dist
  dist.multiple["a", "g"] <- 99

  res.multiple <- matched.distances(match.multiple, dist.multiple, preserve.unit.names = T)
  expect_equal(length(res.multiple), 4) # 4 matches, four item list
  expect_equal(as.vector(unlist(res.multiple)), c(1, 99, 3, 4, 5))
  expect_equal(as.vector(unlist(lapply(res.multiple, names))), c("f", "g", "h", "i", "j"))


})

test_that("Subsetting drops any matched.distances attributes", {
  data(nuclearplants)

  f1 <- fullmatch(glm(pr ~ t1 + ne, data=nuclearplants, family=binomial),
                  within=exactMatch(pr ~ ne, data=nuclearplants),
                  data=nuclearplants)

  expect_true(is.null(attr(f1, "matched.distances")))

  # Add the attribute (because it is no longer created by default)
  attr(f1, "matched.distances") <- runif(length(levels(f1)))

  expect_true(!is.null(attr(f1, "matched.distances")))

  f2 <- f1[1:10]
  f3 <- f1[1:10, drop=TRUE]

  expect_true(is.null(attr(f2, "matched.distances")))
  expect_true(is.null(attr(f3, "matched.distances")))
})

test_that("Summary properly handles matched.distances #106", {
  data(nuclearplants)
  dist <- match_on(glm(pr~.-(pr+cost), family=binomial(),
                       data=nuclearplants))

  pm <- pairmatch(dist, data=nuclearplants)

  s1 <- summary(pm)

  expect_true(is.null(s1$total.distance))

  # if we add matched.distances in manually, should re-appear in
  # summary.
  attr(pm, "matched.distances") <- matched.distances(pm, dist)

  s2 <- summary(pm)

  expect_true(!is.null(s2$total.distance))
  expect_true(!is.null(s2$total.tolerance))
  expect_true(!is.null(s2$matched.dist.quantiles))

  # Double check that the match isn't getting affected.
  expect_identical(s1$thematch[sort(names(s1$thematch))], s2$thematch[sort(names(s2$thematch))])
})

test_that("Match carries info about subproblems", {
  Z <- rep(c(0,1), 8)
  B <- as.factor(rep(c(1,2), each = 8))
  names(Z) <- names(B) <- letters[1:16]
  match <- pairmatch(exactMatch(Z ~ B), data = Z) # assure data order by passing Z

  # subproblem attribute should be a factor indicating which group each item maps to
  expect_equal(class(attr(match, "subproblem"))[1], "factor")
  expect_equal(length(match), length(attr(match, "subproblem")))
  expect_equivalent(B, attr(match, "subproblem"))

})

test_that("Indicating failing subproblems", {
  Z <- rep(c(0,1), 8)
  B <- as.factor(rep(c(1,2), each = 8))
  names(Z) <- names(B) <- letters[1:16]
  match <- pairmatch(exactMatch(Z ~ B), data = Z) # assure data order by passing Z

  spS <- subproblemSuccess(match)
  mf <- matchfailed(match)
  expect_equal(sum(spS), 2)
  expect_true(all(names(spS) %in%  c("1", "2")))
  expect_is(mf, "logical")
  expect_length(mf, length(B))
  expect_true(all(mf == FALSE))

  Z[1] <- 1
  match <- pairmatch(exactMatch(Z ~ B), data = Z)

  spS <- subproblemSuccess(match)
  mf <- matchfailed(match)
  expect_equal(sum(spS), 2)
  expect_true(all(names(spS) %in%  c("1", "2")))
  expect_is(mf, "logical")
  expect_length(mf, length(B))
  expect_true(all(mf == FALSE))


  data(nuclearplants)
  expect_warning(f1 <- fullmatch(pr ~ t1, data = nuclearplants,
                                 min = 5, max = 5))

  spS <- subproblemSuccess(f1)
  mf <- matchfailed(f1)
  expect_true(all(spS == FALSE))
  expect_equal(names(spS), "1")
  expect_is(mf, "logical")
  expect_length(mf, nrow(nuclearplants))
  expect_true(all(mf == TRUE))

  expect_warning(f2 <-
                   fullmatch(pr ~ t1, data = nuclearplants,
                             min = 5, max = 5,
                             within =
                               exactMatch(pr ~ pt,
                                          data = nuclearplants)))

  spS <- subproblemSuccess(f2)
  mf <- matchfailed(f2)
  expect_true(all(spS == FALSE))
  expect_is(mf, "logical")
  expect_length(mf, nrow(nuclearplants))
  expect_true(all(mf == TRUE))

  expect_warning(f3 <-
                   fullmatch(pr ~ cost, data = nuclearplants,
                             min = 60, max = 60,
                             within =
                               exactMatch(pr ~ pt,
                                          data = nuclearplants)))

  spS <- subproblemSuccess(f3)
  mf <- matchfailed(f3)
  expect_true(all(spS == FALSE))
  expect_is(mf, "logical")
  expect_length(mf, nrow(nuclearplants))
  expect_true(all(mf == TRUE))
})

test_that("optmatch_restrictions", {
  d <- data.frame(Z = c(1,0,0,0,0,1,0,0),
                  B = rep(c('a', 'b'), times=c(5, 3)))

  res.b <- exactMatch(Z ~ B, data=d)

  f <- fullmatch(res.b, data=d)
  o <- optmatch_restrictions(f)
  expect_true(all(o$min.controls == 0))
  expect_true(all(o$max.controls == Inf))
  expect_true(all(is.na(o$omit.fraction)))
  expect_true(all(is.null(o$mean.controls)))

  f <- fullmatch(res.b, data=d, mean.controls = 1)
  o <- optmatch_restrictions(f)
  expect_true(all(o$min.controls == 0))
  expect_true(all(o$max.controls == Inf))
  expect_true(all(is.null(o$omit.fraction)))
  expect_true(all(o$mean.controls == 1))

  f <- fullmatch(res.b, data=d, mean.controls = 1, max.controls=c(1,2), min.controls=c(1, 1/2))
  o <- optmatch_restrictions(f)
  expect_true(all(o$min.controls == c(1, 1/2)))
  expect_true(all(o$max.controls == c(1,2)))
  expect_true(all(is.null(o$omit.fraction)))
  expect_true(all(o$mean.controls == 1))

  expect_true(all(names(o$min.controls) == c('a','b')))
  expect_true(all(names(o$max.controls) == c('a','b')))
  expect_true(all(names(o$mean.controls) == c('a','b')))

  options("optmatch_verbose_messaging" = TRUE)
  expect_warning(f <- fullmatch(res.b, data=d, max.controls=1),
                 "infeasible")
  o <- optmatch_restrictions(f)
  expect_true(all(o$min.controls == 0))
  expect_true(all(o$max.controls == 1))
  expect_true(all(o$omit.fraction == c(3/4, 1/2)))
  expect_true(all(is.null(o$mean.controls)))
})

test_that("optmatch_same_distance", {
  d <- data.frame(Z = c(1,0,0,0,0,1,0,0),
                  B = rep(c('a', 'b'), times=c(5, 3)))

  res.b <- exactMatch(Z ~ B, data=d)
  res.b2 <- res.b
  res.b2@.Data[1] <- 1

  f1 <- fullmatch(res.b, data=d)
  f2 <- fullmatch(res.b2, data=d)

  options("optmatch_verbose_messaging" = TRUE)
  expect_warning(f3 <- fullmatch(res.b, data=d, max.controls=1),
                 "infeasible")

  expect_true(optmatch_same_distance(f1, res.b))
  expect_true(optmatch_same_distance(res.b, f1))
  expect_true(optmatch_same_distance(f2, res.b2))
  expect_true(optmatch_same_distance(res.b2, f2))
  expect_true(optmatch_same_distance(f3, res.b))
  expect_true(optmatch_same_distance(res.b, f3))

  expect_true(!optmatch_same_distance(f1, res.b2))
  expect_true(!optmatch_same_distance(f2, res.b))
  expect_true(!optmatch_same_distance(f3, res.b2))

  expect_true(optmatch_same_distance(f1, f3))
  expect_true(!optmatch_same_distance(f1, f2))

  expect_true(optmatch_same_distance(res.b, res.b))
  expect_error(optmatch_same_distance(f1, as.matrix(res.b)), "both arguments")
})


test_that("update.optmatch basics", {
  d <- data.frame(z = rep(0:1, each = 50),
                  b = rnorm(100))

  # update without arguments shouldn't change anything
  f1 <- fullmatch(z ~ b, data = d)
  expect_is(update(f1), "optmatch")
  expect_true(identical(f1, update(f1)))
})

test_that("update without changing distance", {
  options("optmatch_verbose_messaging" = FALSE)

  d <- data.frame(z = rep(0:1, each = 50),
                  b = rnorm(100))

  f1 <- fullmatch(z ~ b, data = d)
  f2 <- fullmatch(z ~ b, data = d, max.controls = 2)
  f3 <- fullmatch(z ~ b, data = d, max.controls = 1)
  f4 <- fullmatch(z ~ b, data = d, max.controls = 1,
                  min.controls = 1)
  f5 <- fullmatch(z ~ b, data = d, omit.fraction = 1/7)
  f6 <- fullmatch(z ~ b, data = d, mean.controls = 1)
  f7 <- fullmatch(z ~ b, data = d, tol = .00001)

  expect_true(identical(f2, update(f1, max.controls = 2)))
  expect_true(identical(f3, update(f1, max.controls = 1)))
  expect_true(identical(f4, update(f1, max.controls = 1,
                                   min.controls = 1)))
  expect_true(identical(f5, update(f1, omit.fraction = 1/7)))
  expect_true(identical(f6, update(f1, mean.controls = 1)))
  expect_true(identical(f7, update(f1, tol = .00001)))
})

test_that("upadate passing a different distance as x argument", {
  options("optmatch_verbose_messaging" = FALSE)

  # passing a difference distance
  set.seed(9876)
  d1 <- data.frame(x = rnorm(10),
                   y = runif(10),
                   z = c(rep(0,6), rep(1,4)))

  res.b1 <- match_on(z ~ x, data = d1)
  res.b2 <- match_on(z ~ y, data = d1)

  f1 <- fullmatch(res.b1, data = d1)
  f2 <- fullmatch(res.b2, data = d1)

  expect_true(!identical(as.vector(f1),as.vector(f2)))

  # When verbose messaging is off, this should produce no distance warning
  options("optmatch_verbose_messaging" = FALSE)
  u1 <- update(f2, x = res.b1)
  u2 <- update(f1, x = res.b2)
  expect_true(identical(f1,u1))
  expect_true(identical(f2,u2))
  expect_true(!identical(f2,u1))
  expect_true(!identical(as.vector(f2),as.vector(u1)))


  # If verbose messaing is enabled, should produce warning
  options("optmatch_verbose_messaging" = TRUE)
  expect_warning(update(f2, x = res.b1), "different than distance")
  expect_warning(update(f1, x = res.b2), "different than distance")
  options("optmatch_verbose_messaging" = FALSE)

  # ensure changing distance + other arguments works
  f3 <- fullmatch(res.b1, data = d1, max.controls = 2)
  u3a <- update(f1, max.controls = 2)
  u3b <- update(f2, x = res.b1, max.controls = 2)

  expect_true(identical(f3, u3a))
  expect_true(identical(f3, u3b))

})

test_that("update when distance is changed outside of update", {
  options("optmatch_verbose_messaging" = FALSE)

  set.seed(9876)
  d1 <- data.frame(x = rnorm(10),
                   y = runif(10),
                   z = c(rep(0,6), rep(1,4)))

  res.c <- match_on(z ~ x, data = d1)

  fc <- fullmatch(res.c, data = d1)

  res.c <- match_on(z ~ y, data = d1)

  uc <- update(fc, x = res.c)
  expect_true(!identical(as.vector(fc), as.vector(uc)))

  # verbose should produce warning

  options("optmatch_verbose_messaging" = TRUE)
  expect_warning(update(fc, x = res.c), "different than distance")
})

test_that("Update arguments change be ordered differently", {
  options("optmatch_verbose_messaging" = FALSE)

  set.seed(9876)
  d1 <- data.frame(x = rnorm(10),
                   y = runif(10),
                   z = c(rep(0,6), rep(1,4)))
  res.c <- match_on(z ~ y, data = d1)
  # odd ordering of parameters

  fo <- fullmatch(data = d1, x = res.c)
  uo <- update(fo, max.controls = 2)
  fo <- fullmatch(data = d1, x = res.c, max.controls = 2)

  expect_true(identical(fo, uo))
})

test_that("Update supporting new formula", {
  data(nuclearplants)

  f1 <- fullmatch(pr ~ cost, data = nuclearplants)
  f2 <- fullmatch(pr ~ t1, data = nuclearplants)

  options("optmatch_verbose_messaging" = FALSE)
  expect_error(update(f2, pr ~ cost), "must be named")
  f3 <- update(f2, x = pr ~ cost)
  expect_identical(f1, f3)
  expect_identical(update(f1, x = pr ~ cost + t1),
                   update(f2, x = pr ~ cost + t1))
})

test_that("update warning for implicit distance changes", {
  data("nuclearplants")
  p <- pairmatch(pr ~ cap, data = nuclearplants)

  # Calipering
  expect_warning(expect_is(up <- update(p, caliper = 1.5),
                           "optmatch"),
                 "different than distance")

  pcal <- pairmatch(pr ~ cap, data = nuclearplants, caliper = 1.5)
  expect_identical(up, pcal)

  # Within
  em <- exactMatch(pr ~ pt, data = nuclearplants)

  expect_warning(uem <- update(p, within = em),
                 "different than distance")

  pe <- pairmatch(pr ~ cap, data = nuclearplants, within = em)

  expect_identical(pe, uem)
})

test_that("update producing errors properly", {
  data(nuclearplants)
  f <- fullmatch(pr ~ cost, data = nuclearplants)
  call <- attr(f, "call")
  attr(f, "call") <- NULL
  expect_error(update(f), "must have a call")
  attr(f, "call") <- 7
  expect_error(update(f), "not a valid")
  attr(f, "call") <- list(call, call)
  expect_error(update(f), "combined optmatch")
})

test_that("num_eligible_matches", {

  options("optmatch_verbose_messaging" = TRUE)
  a <- matrix(rep(0,9), nrow=3)
  class(a) <- c("DenseMatrix", class(a))
  expect_true(num_eligible_matches(a) == 9)

  a[1] <- Inf
  expect_true(num_eligible_matches(a) == 8)

  b <- makeInfinitySparseMatrix(1:4,
                                rows=c(1L,1L,2L,3L),
                                cols=c(1L,2L,3L,3L),
                                dimension=c(3L,3L),
                                colnames=letters[1:3],
                                rownames=LETTERS[1:3])

  expect_true(num_eligible_matches(b) == 4)
  c <- b
  c[1] <- Inf
  expect_true(num_eligible_matches(c) == 3)

  d <- as(b, "BlockedInfinitySparseMatrix")
  d@groups <- factor(c("cat","cat","dog","cat","cat","dog"))
  names(d@groups) <- c(LETTERS[1:3], letters[1:3])

  nem <- num_eligible_matches(d)
  expect_equal(names(nem), c("cat", "dog"))
  expect_equal(nem[[1]], 3)
  expect_equal(nem[[2]], 1)
  expect_true(num_eligible_matches.InfinitySparseMatrix(d) == 4)
  expect_true(num_eligible_matches(as.InfinitySparseMatrix(d)) == 4)

  e <- d
  e[1] <- Inf

  nem <- num_eligible_matches(e)
  expect_equal(names(nem), c("cat", "dog"))
  expect_equal(nem[[1]], 2)
  expect_equal(nem[[2]], 1)
  expect_true(num_eligible_matches.InfinitySparseMatrix(e) == 3)
  expect_true(num_eligible_matches(as.InfinitySparseMatrix(e)) == 3)


  d <- matrix(rep(1:2, 10), 10, 2)
  d <- caliper(d, 1.5)
  expect_true(num_eligible_matches(d) == 10)

})

test_that("equality of matches", {
  data(nuclearplants)

  # Truly identical matches
  f1 <- fullmatch(pr ~ cost, data=nuclearplants)
  f2 <- fullmatch(pr ~ cost, data=nuclearplants)

  expect_true(compare_optmatch(f1, f2))

  # Completely different matched
  data(plantdist)
  expect_warning(p1 <- fullmatch(plantdist), "Without \'data\'")

  expect_false(compare_optmatch(p1,f1))

  # Same match, different call
  f3 <- fullmatch(pr ~ cost, data=nuclearplants, max=100)

  expect_true(compare_optmatch(f1, f3))

  # Matches with unmatched objects
  expect_warning(f4 <- fullmatch(pr ~ cost, data=nuclearplants, max=1), "infeasible")
  expect_warning(f5 <- fullmatch(pr ~ cost, data=nuclearplants, max=1, min=1), "infeasible")

  expect_true(compare_optmatch(f4,f5))

  # Make sure its not returning true for everything!
  expect_false(compare_optmatch(f1,f4))
  expect_false(compare_optmatch(f3,f5))

  # Re-ordering
  nuclearplants2 <- nuclearplants[sample(seq_len(nrow(nuclearplants))),]

  f6 <- fullmatch(pr ~ cost, data=nuclearplants2)
  # f1, f2, f6 are all the same, but f6 has a different order
  expect_true(all(f1 == f2))
  expect_false(all(f1 == f6))
  # But compare_optmatch doesn't care!
  expect_true(compare_optmatch(f1, f6))

  # Try with blocked
  b1 <- fullmatch(pr ~ cost, data=nuclearplants, within=exactMatch(pr ~ ne, data=nuclearplants))
  nuclearplants$ne2 <- 1 - nuclearplants$ne
  b2 <- fullmatch(pr ~ cost, data=nuclearplants, within=exactMatch(pr ~ ne2, data=nuclearplants))

  expect_error(all(b1 == b2), "sets of factors are different")
  # But compare_optmatch doesn't care!
  expect_true(compare_optmatch(b1, b2))

  # Make some wonky observation names
  row.names(nuclearplants) <-
    vapply(seq_len(nrow(nuclearplants)),
           function(x) {
             paste0(sample(strsplit("!@#$%^&*()_+1234567890asdfghjkl",
                                    "")[[1]], 10, TRUE),
                    collapse="")
           }, character(1)
           )

  w1 <- fullmatch(pr ~ cost, data=nuclearplants)
  w2 <- fullmatch(pr ~ cost, data=nuclearplants, max=10)
  expect_true(compare_optmatch(w1,w2))

  wb1 <- fullmatch(pr ~ cost, data=nuclearplants, within=exactMatch(pr ~ ne, data=nuclearplants))
  wb2 <- fullmatch(pr ~ cost, data=nuclearplants, within=exactMatch(pr ~ ne2, data=nuclearplants))

  expect_error(all(wb1 == wb2), "sets of factors are different")
  # But compare_optmatch doesn't care!
  expect_true(compare_optmatch(wb1, wb2))

  # If we drop NA members, should be the same match
  f4_dropna<- f4[!is.na(f4)]
  expect_true(compare_optmatch(f4, f4_dropna))

  # The problem that motivated this function: Two matches are identical, except one has an extra NA
  f4b <- f4
  f4b[1] <- NA

  # This doesn't catch it!
  expect_true(all(f4 == f4b, na.rm=TRUE))

  # This does!
  expect_false(compare_optmatch(f4, f4b))

  # Differing names should always be false.
  f1b <- f1
  names(f1b)[1] <- "Z"
  expect_false(compare_optmatch(f1, f1b))

  f1c <- f1
  names(f1c)[1] <- "A"
  expect_false(compare_optmatch(f1, f1c))

  ## # Saving this to test time.
  ## n <- 20000
  ## s1 <- as.factor(sample(letters, n, TRUE))
  ## names(s1) <- sample(LETTERS, n, TRUE)
  ## s2 <- s1[sample(seq_along(s1), n, TRUE)]
  ## system.time(compare_optmatch(s1,s2))
  ## # Taking about .3sec on laptop.

})

test_that("combining optmatch objects", {
  data(nuclearplants)
  f1 <- fullmatch(pr ~ t1, data = nuclearplants[nuclearplants$pt == 0,])

  expect_is(c(f1), "optmatch")

  f2 <- fullmatch(pr ~ t1, data = nuclearplants[nuclearplants$pt == 1,])

  fc <- c(f1, f2)

  expect_equal(length(fc), length(f1) + length(f2))
  for (a in c("subproblem", "contrast.group", "levels")) {
    expect_equal(length(attr(fc, a)),
                 length(attr(f1, a)) + length(attr(f2, a)))
  }

  expect_is(attr(fc, "hashed.distance"), "list")
  expect_length(attr(fc, "hashed.distance"), 2)
  expect_is(attr(fc, "call"), "list")
  expect_length(attr(fc, "call"), 2)

  for (a in c("min.controls", "max.controls", "omit.fraction", "exceedances")) {
    expect_is(attr(fc, a), "numeric")
    expect_length(attr(fc, a), 2)
    expect_equivalent(attr(fc, a)[1], attr(f1, a))
    expect_equivalent(attr(fc, a)[2], attr(f2, a))
  }


  expect_error(c(f1, f1), "duplicated")

  full <- fullmatch(pr ~ t1, data = nuclearplants,
                    within = exactMatch(pr ~ pt, data = nuclearplants))

  expect_true(compare_optmatch(fc, full))

  levels(full) <- levels(fc)
  expect_equivalent(full, fc)

  p1 <- pairmatch(pr ~ t1, data = nuclearplants[nuclearplants$pt == 0,])

  expect_is(c(p1), "optmatch")

  p2 <- pairmatch(pr ~ t1, data = nuclearplants[nuclearplants$pt == 1,])

  pc <- c(p1, p2)

  expect_equal(length(pc), length(p1) + length(p2))
  for (a in c("subproblem", "contrast.group", "levels")) {
    expect_equal(length(attr(pc, a)),
                 length(attr(p1, a)) + length(attr(p2, a)))
  }

  expect_error(c(p1, p1), "duplicated")

  expect_identical(is.na(p1), is.na(pc)[1:26])
  expect_identical(is.na(p2), is.na(pc)[27:32])

  f1 <- fullmatch(pr ~ t1, data = nuclearplants[1:10,])
  f2 <- fullmatch(pr ~ t1, data = nuclearplants[11:25,])
  f3 <- fullmatch(pr ~ t1, data = nuclearplants[26:32,])

  fc <- c(f1, f2, f3)
  expect_is(fc, "optmatch")

  expect_equal(length(fc), length(f1) + length(f2) + length(f3))
  for (a in c("subproblem", "contrast.group", "levels")) {
    expect_equal(length(attr(fc, a)),
                 length(attr(f1, a)) + length(attr(f2, a)) +
                   length(attr(f3, a)))
  }

  expect_is(attr(fc, "hashed.distance"), "list")
  expect_length(attr(fc, "hashed.distance"), 3)
  expect_is(attr(fc, "call"), "list")
  expect_length(attr(fc, "call"), 3)

  for (a in c("min.controls", "max.controls", "omit.fraction", "exceedances")) {
    expect_is(attr(fc, a), "numeric")
    expect_length(attr(fc, a), 3)
    expect_equivalent(attr(fc, a)[1], attr(f1, a))
    expect_equivalent(attr(fc, a)[2], attr(f2, a))
    expect_equivalent(attr(fc, a)[3], attr(f3, a))
  }

  # Min, Max, etc carry forward properly
  options("optmatch_verbose_messaging" = FALSE)
  f1 <- fullmatch(pr ~ t1, data = nuclearplants[1:25,],
                  min = 1, max = 2)

  f2 <- fullmatch(pr ~ t1, data = nuclearplants[26:32,],
                  max = 3, omit.fraction = .1)

  fc <- c(f1, f2)

  expect_equivalent(attr(fc, "max.controls"),
                    c(attr(f1, "max.controls"),
                      attr(f2, "max.controls")))
  expect_equivalent(attr(fc, "min.controls"),
                    c(attr(f1, "min.controls"),
                      attr(f2, "min.controls")))
  expect_equivalent(attr(fc, "omit.fraction"),
                    c(attr(f1, "omit.fraction"),
                      attr(f2, "omit.fraction")))

  # Functions taking optmatch objects

  f1 <- fullmatch(pr ~ t1, data = nuclearplants[1:25,],
                  min = 1, max = 2)
  f2 <- fullmatch(pr ~ t1, data = nuclearplants[26:32,],
                  min = 1, max = 2)
  fc <- c(f1, f2)
  nuclearplants$treat <- rep(0:1, times = c(25, 7))
  full <- fullmatch(pr ~ t1, data = nuclearplants, min = 1, max = 2,
                    within = exactMatch(pr ~ treat, data = nuclearplants))

  expect_identical(matched(fc), matched(full))

  expect_identical(optmatch_restrictions(fc), optmatch_restrictions(full))
  expect_identical(stratumStructure(fc), stratumStructure(full))
  expect_identical(summary(fc)$effective.sample.size,
                   summary(full)$effective.sample.size)
  expect_identical(summary(fc)$matched.set.structures,
                   summary(full)$matched.set.structures)

  # Suppress output, but will error
  expect_silent(invisible(capture.output(print(fc))))
  expect_silent(invisible(capture.output(print(fc, quote = TRUE))))
  expect_silent(invisible(capture.output(print(fc, grouped = TRUE))))

  expect_output(print(fc), "0.1.1")
  expect_output(print(fc), "1.1.1")
  expect_output(print(fc, grouped = TRUE), "0.1.1")
  expect_output(print(fc, grouped = TRUE), "1.1.1")
  expect_output(print(fc, grouped = TRUE), "Members")

})

test_that("combining already blocked matches", {
  data("nuclearplants")
  nuclearplants$z <- rep(0:2, times = c(15,10,7))
  f1 <- fullmatch(pr ~ t1, data = nuclearplants[nuclearplants$z == 0,])
  f2 <- fullmatch(pr ~ t1, data = nuclearplants[nuclearplants$z != 0,],
                  within = exactMatch(pr ~ z, data = nuclearplants))
  fc <- c(f1, f2)
  full <- fullmatch(pr ~ t1, data = nuclearplants,
                    within = exactMatch(pr ~ z, data = nuclearplants))

  # There are a few matches with equivalent mincost solutions, so just ensure
  # total matched distance is equivalent.
  expect_true(all.equal(
    sum(Reduce(c, matched.distances(fc,
                                match_on(pr ~ t1,
                                         data = nuclearplants,
                                         within =
                                           exactMatch(pr ~ z,
                                                      data = nuclearplants))))),
    sum(Reduce(c, matched.distances(full,
                                match_on(pr ~ t1,
                                         data = nuclearplants,
                                         within =
                                           exactMatch(pr ~ z,
                                                      data = nuclearplants)))))
  ))

  expect_identical(matched(fc), matched(full))


  expect_equivalent(attr(fc, "max.controls"),
                    attr(full, "max.controls"))
  expect_equivalent(attr(fc, "min.controls"),
                    attr(full, "min.controls"))
  expect_equivalent(attr(fc, "omit.fraction"),
                    attr(full, "omit.fraction"))
  expect_equivalent(attr(fc, "exceedances"),
                    c(attr(f1, "exceedances"),
                      attr(f2, "exceedances")))
  expect_identical(stratumStructure(fc), stratumStructure(full))
  expect_identical(summary(fc)$effective.sample.size,
                   summary(full)$effective.sample.size)
  expect_identical(summary(fc)$matched.set.structures,
                   summary(full)$matched.set.structures)
})

test_that("handleSolver", {
  # Input: ""
  s <- handleSolver("")
  if (requireNamespace("rrelaxiv", quietly = TRUE)) {
    expect_equal(s, "RELAX-IV")
  } else {
    expect_equal(s, LEMON())
  }

  # Input: "RELAX-IV"
  if (requireNamespace("rrelaxiv", quietly = TRUE)) {
    s <- handleSolver("RELAX-IV")
    expect_equal(s, "RELAX-IV")
  } else {
    expect_error(handleSolver("RELAX-IV"),
                 "install package")
  }

  # Input: "LEMON"
  s <- handleSolver("LEMON")
  expect_equal(s, LEMON())

  # INPUT: LEMON(...)
  s <- handleSolver(LEMON("CycleCancelling"))
  expect_equal(s, LEMON("CycleCancelling"))
  s <- handleSolver(LEMON("CostScaling"))
  expect_equal(s, LEMON("CostScaling"))
  s <- handleSolver(LEMON("CapacityScaling"))
  expect_equal(s, LEMON("CapacityScaling"))
  s <- handleSolver(LEMON("NetworkSimplex"))
  expect_equal(s, LEMON("NetworkSimplex"))

  expect_error(handleSolver("ABC"), "Invalid solver")
  expect_error(handleSolver(123), "Invalid solver")
  expect_error(handleSolver(ls()), "Invalid solver")


})
