
test_that("sfc_writer() works with fixed-length input", {
  skip_if_not_installed("sf")

  # zero-length
  expect_identical(wk_handle(wkb(), sfc_writer()), sf::st_sfc())

  expect_identical(
    wk_handle(
      as_wkt(
        c("POINT EMPTY", "LINESTRING EMPTY", "POLYGON EMPTY",
          "MULTIPOINT EMPTY", "MULTILINESTRING EMPTY", "MULTIPOLYGON EMPTY",
          "GEOMETRYCOLLECTION EMPTY"
        )
      ),
      sfc_writer()
    ),
    sf::st_sfc(
      sf::st_point(), sf::st_linestring(), sf::st_polygon(),
      sf::st_multipoint(), sf::st_multilinestring(), sf::st_multipolygon(),
      sf::st_geometrycollection()
    )
  )

  expect_identical(
    wk_handle(as_wkb("POINT (1 1)"), sfc_writer()),
    sf::st_sfc(sf::st_point(c(1, 1)))
  )

  expect_identical(
    wk_handle(as_wkb("LINESTRING (1 2, 3 4)"), sfc_writer()),
    sf::st_sfc(sf::st_linestring(rbind(c(1, 2), c(3, 4))))
  )

  expect_identical(
    wk_handle(as_wkb("POLYGON ((0 0, 0 1, 1 0, 0 0))"), sfc_writer()),
    sf::st_sfc(sf::st_polygon(list(rbind(c(0, 0), c(0, 1), c(1, 0), c(0, 0)))))
  )

  expect_identical(
    wk_handle(as_wkb("MULTIPOINT ((1 2), (3 4))"), sfc_writer()),
    sf::st_sfc(sf::st_multipoint(rbind(c(1, 2), c(3, 4))))
  )

  expect_identical(
    wk_handle(as_wkb("MULTILINESTRING ((1 1, 2 2), (2 2, 3 4))"), sfc_writer()),
    sf::st_sfc(
      sf::st_multilinestring(
        list(rbind(c(1, 1), c(2, 2)), rbind(c(2, 2), c(3, 4)))
      )
    )
  )

  expect_identical(
    wk_handle(
      as_wkb("MULTIPOLYGON (((0 0, 0 1, 1 0, 0 0)), ((0 0, 0 -2, -1 0, 0 0)))"),
      sfc_writer()
    ),
    sf::st_sfc(
      sf::st_multipolygon(
        list(
          list(rbind(c(0, 0), c(0, 1), c(1, 0), c(0, 0))),
          list(rbind(c(0, 0), c(0, -2), c(-1, 0), c(0, 0)))
        )
      )
    )
  )

  expect_identical(
    wk_handle(as_wkb("GEOMETRYCOLLECTION (POINT (1 1), LINESTRING (1 1, 2 2))"), sfc_writer()),
    sf::st_sfc(
      sf::st_geometrycollection(
        list(
          sf::st_point(c(1, 1)),
          sf::st_linestring(rbind(c(1, 1), c(2, 2)))
        )
      )
    )
  )
})

test_that("sfc_writer() works with promote_multi = TRUE", {
  skip_if_not_installed("sf")

  expect_identical(
    wk_handle(
      as_wkt(
        c("POINT EMPTY", "LINESTRING EMPTY", "POLYGON EMPTY",
          "MULTIPOINT EMPTY", "MULTILINESTRING EMPTY", "MULTIPOLYGON EMPTY",
          "GEOMETRYCOLLECTION EMPTY"
        )
      ),
      sfc_writer(promote_multi = TRUE)
    ),
    sf::st_sfc(
      sf::st_multipoint(), sf::st_multilinestring(), sf::st_multipolygon(),
      sf::st_multipoint(), sf::st_multilinestring(), sf::st_multipolygon(),
      sf::st_geometrycollection()
    )
  )

  expect_identical(
    wk_handle(as_wkb("POINT (1 1)"), sfc_writer(promote_multi = TRUE)),
    sf::st_sfc(sf::st_multipoint(matrix(c(1, 1), ncol = 2)))
  )

  expect_identical(
    wk_handle(as_wkb("POINT Z (1 1 2)"), sfc_writer(promote_multi = TRUE)),
    sf::st_sfc(sf::st_multipoint(matrix(c(1, 1, 2), ncol = 3)))
  )

  expect_identical(
    wk_handle(as_wkb("POINT M (1 1 2)"), sfc_writer(promote_multi = TRUE)),
    sf::st_sfc(sf::st_multipoint(matrix(c(1, 1, 2), ncol = 3), dim = "XYM"))
  )

  expect_identical(
    wk_handle(as_wkb("POINT ZM (1 1 2 3)"), sfc_writer(promote_multi = TRUE)),
    sf::st_sfc(sf::st_multipoint(matrix(c(1, 1, 2, 3), ncol = 4)))
  )

  expect_identical(
    wk_handle(as_wkb("LINESTRING (1 2, 3 4)"), sfc_writer(promote_multi = TRUE)),
    sf::st_sfc(sf::st_multilinestring(list(rbind(c(1, 2), c(3, 4)))))
  )

  expect_identical(
    wk_handle(as_wkb("POLYGON ((0 0, 0 1, 1 0, 0 0))"), sfc_writer(promote_multi = TRUE)),
    sf::st_sfc(sf::st_multipolygon(list(list(rbind(c(0, 0), c(0, 1), c(1, 0), c(0, 0))))))
  )

  expect_identical(
    wk_handle(as_wkb("MULTIPOINT ((1 2), (3 4))"), sfc_writer(promote_multi = TRUE)),
    sf::st_sfc(sf::st_multipoint(rbind(c(1, 2), c(3, 4))))
  )

  expect_identical(
    wk_handle(as_wkb("MULTILINESTRING ((1 1, 2 2), (2 2, 3 4))"), sfc_writer(promote_multi = TRUE)),
    sf::st_sfc(
      sf::st_multilinestring(
        list(rbind(c(1, 1), c(2, 2)), rbind(c(2, 2), c(3, 4)))
      )
    )
  )

  expect_identical(
    wk_handle(
      as_wkb("MULTIPOLYGON (((0 0, 0 1, 1 0, 0 0)), ((0 0, 0 -2, -1 0, 0 0)))"),
      sfc_writer(promote_multi = TRUE)
    ),
    sf::st_sfc(
      sf::st_multipolygon(
        list(
          list(rbind(c(0, 0), c(0, 1), c(1, 0), c(0, 0))),
          list(rbind(c(0, 0), c(0, -2), c(-1, 0), c(0, 0)))
        )
      )
    )
  )

  expect_identical(
    wk_handle(
      as_wkb("GEOMETRYCOLLECTION (POINT (1 1), LINESTRING (1 1, 2 2))"),
      sfc_writer(promote_multi = TRUE)
    ),
    sf::st_sfc(
      sf::st_geometrycollection(
        list(
          sf::st_point(c(1, 1)),
          sf::st_linestring(rbind(c(1, 1), c(2, 2)))
        )
      )
    )
  )
})

test_that("nested points are treated the same as top-level points", {
  skip_if_not_installed("sf")

  non_empty_nested <- as_wkt(c("GEOMETRYCOLLECTION (POINT (1 2))", "POINT EMPTY"))
  empty_nested <- as_wkt(c("GEOMETRYCOLLECTION (POINT EMPTY)", "POINT (1 2)"))

  expect_identical(
    sf::st_bbox(wk_handle(non_empty_nested, sfc_writer())),
    sf::st_bbox(wk_handle(empty_nested, sfc_writer())),
  )
})

test_that("sfc_writer() turns NULLs into EMPTY", {
  expect_identical(
    wk_handle(wkb(list(NULL)), sfc_writer()),
    wk_handle(wkt("GEOMETRYCOLLECTION EMPTY"), sfc_writer())
  )

  all_types <- as_wkb(
    c("POINT EMPTY", "LINESTRING EMPTY", "POLYGON EMPTY",
      "MULTIPOINT EMPTY", "MULTILINESTRING EMPTY", "MULTIPOLYGON EMPTY",
      "GEOMETRYCOLLECTION EMPTY"
    )
  )

  for (i in seq_along(all_types)) {
    expect_identical(
      wk_handle(c(all_types[i], wkb(list(NULL))), sfc_writer()),
      wk_handle(c(all_types[i], all_types[i]), sfc_writer())
    )
  }

  expect_identical(
    wk_handle(c(all_types[1:2], wkb(list(NULL))), sfc_writer()),
    wk_handle(c(all_types[1:2], as_wkb("GEOMETRYCOLLECTION EMPTY")), sfc_writer())
  )

  all_types_non_empty <- as_wkb(
    c(
      "POINT (1 2)", "LINESTRING (1 2, 3 4)",
      "POLYGON ((0 0, 0 1, 1 0, 0 0))",
      "MULTIPOINT ((1 2), (3 4))",
      "MULTILINESTRING ((1 2, 3 4))",
      "MULTIPOLYGON (((0 0, 0 1, 1 0, 0 0)), ((0 0, 0 -2, -1 0, 0 0)))",
      "GEOMETRYCOLLECTION (POINT (1 2))"
    )
  )

  types <- c(
    "POINT", "LINESTRING", "POLYGON",
    "MULTIPOINT", "MULTILINESTRING", "MULTIPOLYGON",
    "GEOMETRYCOLLECTION"
  )

  for (i in seq_along(all_types)) {
    vec <- wk_handle(c(all_types_non_empty[i], wkb(list(NULL))), sfc_writer())
    expect_identical(vec[[2]], wk_handle(all_types[i], sfc_writer())[[1]])
    expect_s3_class(vec, paste0("sfc_", types[i]))
  }

  # check at least one Z, M, and ZM geometry
  zm_types <- as_wkb(
    c("POINT ZM (1 2 3 4)", "POINT Z (1 2 3)", "POINT M (1 2 3)")
  )

  zm_types_empty <- as_wkb(
    c("POINT ZM EMPTY", "POINT Z EMPTY", "POINT M EMPTY")
  )

  for (i in seq_along(all_types)) {
    expect_identical(
      wk_handle(c(zm_types[i], wkb(list(NULL))), sfc_writer()),
      wk_handle(c(zm_types[i], zm_types_empty[i]), sfc_writer())
    )
  }
})

test_that("sfc_writer() reproduces all basic geometry types for WKB input", {
  skip_if_not_installed("sf")

  nc <- sf::read_sf(system.file("shape/nc.shp", package = "sf"))
  nc_multipolygon <- sf::st_set_crs(nc$geometry, NA)
  nc_multilines <- sf::st_boundary(nc_multipolygon)
  nc_multipoints <- sf::st_cast(nc_multilines, "MULTIPOINT")
  nc_polygon <- sf::st_cast(nc_multipolygon, "POLYGON")
  nc_lines <- sf::st_cast(nc_multilines, "LINESTRING")
  nc_points <- sf::st_cast(nc_lines, "POINT")
  collection_list <- nc_multipolygon
  attributes(collection_list) <- NULL
  nc_collection <- sf::st_sfc(sf::st_geometrycollection(collection_list))

  attr(nc_multipoints, "ids") <- NULL
  attr(nc_polygon, "ids") <- NULL
  attr(nc_lines, "ids") <- NULL
  attr(nc_points, "ids") <- NULL

  expect_identical(
    wk_handle(as_wkb(nc_multipolygon), sfc_writer()),
    nc_multipolygon
  )

  expect_identical(
    wk_handle(as_wkb(nc_multilines), sfc_writer()),
    nc_multilines
  )

  expect_identical(
    wk_handle(as_wkb(nc_multipoints), sfc_writer()),
    nc_multipoints
  )

  expect_identical(
    wk_handle(as_wkb(nc_polygon), sfc_writer()),
    nc_polygon
  )

  expect_identical(
    wk_handle(as_wkb(nc_lines), sfc_writer()),
    nc_lines
  )

  expect_identical(
    wk_handle(as_wkb(nc_points), sfc_writer()),
    nc_points
  )

  expect_identical(
    wk_handle(as_wkb(nc_collection), sfc_writer()),
    nc_collection
  )
})

test_that("sfc_writer() reproduces all basic geometry types for WKT input", {
  skip_if_not_installed("sf")

  nc <- sf::read_sf(system.file("shape/nc.shp", package = "sf"))
  nc_multipolygon <- sf::st_set_crs(nc$geometry, NA)
  nc_multilines <- sf::st_boundary(nc_multipolygon)
  nc_multipoints <- sf::st_cast(nc_multilines, "MULTIPOINT")
  nc_polygon <- sf::st_cast(nc_multipolygon, "POLYGON")
  nc_lines <- sf::st_cast(nc_multilines, "LINESTRING")
  nc_points <- sf::st_cast(nc_lines, "POINT")
  collection_list <- nc_multipolygon
  attributes(collection_list) <- NULL
  nc_collection <- sf::st_sfc(sf::st_geometrycollection(collection_list))

  attr(nc_multipoints, "ids") <- NULL
  attr(nc_polygon, "ids") <- NULL
  attr(nc_lines, "ids") <- NULL
  attr(nc_points, "ids") <- NULL

  expect_equal(
    wk_handle(as_wkt(nc_multipolygon), sfc_writer()),
    nc_multipolygon
  )

  expect_equal(
    wk_handle(as_wkt(nc_multilines), sfc_writer()),
    nc_multilines
  )

  expect_equal(
    wk_handle(as_wkt(nc_multipoints), sfc_writer()),
    nc_multipoints
  )

  expect_equal(
    wk_handle(as_wkt(nc_polygon), sfc_writer()),
    nc_polygon
  )

  expect_equal(
    wk_handle(as_wkt(nc_lines), sfc_writer()),
    nc_lines
  )

  expect_equal(
    wk_handle(as_wkt(nc_points), sfc_writer()),
    nc_points
  )

  expect_equal(
    wk_handle(as_wkt(nc_collection), sfc_writer()),
    nc_collection
  )
})

test_that("sfc writer works with ZM dimensions", {
  skip_if_not_installed("sf")

  expect_identical(
    wk_handle(wkt(c("POINT ZM (1 2 3 4)", "POINT ZM EMPTY")), sfc_writer()),
    sf::st_sfc(sf::st_point(c(1, 2, 3, 4)), sf::st_point(rep(NA_real_, 4), dim = "XYZM"))
  )

  expect_identical(
    wk_handle(wkt(c("POINT Z (1 2 3)", "POINT Z EMPTY")), sfc_writer()),
    sf::st_sfc(sf::st_point(c(1, 2, 3)), sf::st_point(rep(NA_real_, 3), dim = "XYZ"))
  )

  expect_identical(
    wk_handle(wkt(c("POINT M (1 2 3)", "POINT M EMPTY")), sfc_writer()),
    sf::st_sfc(sf::st_point(c(1, 2, 3), dim = "XYM"), sf::st_point(rep(NA_real_, 3), dim = "XYM"))
  )

  expect_identical(
    wk_handle(wkt(c("LINESTRING ZM (1 2 3 4, 5 6 7 8)", "LINESTRING ZM EMPTY")), sfc_writer()),
    sf::st_sfc(
      sf::st_linestring(rbind(c(1, 2, 3, 4), c(5, 6, 7, 8))),
      sf::st_linestring(matrix(double(), ncol = 4), dim = "XYZM")
    )
  )

  expect_identical(
    wk_handle(wkt(c("LINESTRING Z (1 2 3, 5 6 7)", "LINESTRING Z EMPTY")), sfc_writer()),
    sf::st_sfc(
      sf::st_linestring(rbind(c(1, 2, 3), c(5, 6, 7)), dim = "XYZ"),
      sf::st_linestring(matrix(double(), ncol = 3), dim = "XYZ")
    )
  )

  expect_identical(
    wk_handle(wkt(c("LINESTRING M (1 2 3, 5 6 7)", "LINESTRING M EMPTY")), sfc_writer()),
    sf::st_sfc(
      sf::st_linestring(rbind(c(1, 2, 3), c(5, 6, 7)), dim = "XYM"),
      sf::st_linestring(matrix(double(), ncol = 3), dim = "XYM")
    )
  )
})

test_that("nested geometries have their dimensions checked", {
  skip_if_not_installed("sf")

  expect_identical(
    wk_handle(wkt("GEOMETRYCOLLECTION Z (POINT Z (1 2 3))"), sfc_writer()),
    sf::st_sfc(sf::st_geometrycollection(list(sf::st_point(c(1, 2, 3), dim = "XYZ")), dims = "XYZ"))
  )

  expect_identical(
    wk_handle(wkt("GEOMETRYCOLLECTION Z (LINESTRING Z (1 2 3, 4 5 6))"), sfc_writer()),
    sf::st_sfc(
      sf::st_geometrycollection(
        list(sf::st_linestring(rbind(c(1, 2, 3), c(4, 5, 6)), dim = "XYZ")),
        dims = "XYZ"
      )
    )
  )

  # note that this is stricter than sf::st_sfc(), which either drops the missing dimension
  # on the GEOMETRYCOLLECTION (when creating from R) or assigns 0 to the missing dimension
  # (when creating from WKT)
  expect_error(
    wk_handle(wkt("GEOMETRYCOLLECTION Z (POINT (1 1))"), sfc_writer()),
    "incompatible dimensions"
  )
  expect_error(
    wk_handle(wkt("GEOMETRYCOLLECTION Z (POINT (1 1))"), sfc_writer()),
    "incompatible dimensions"
  )
})

test_that("nested empties result in NA ranges", {
  skip_if_not_installed("sf")

  expect_identical(
    sf::st_bbox(wk_handle(wkt("GEOMETRYCOLLECTION ZM (POINT EMPTY)"), sfc_writer())),
    sf::st_bbox(sf::st_as_sfc("POINT ZM EMPTY"))
  )

  expect_identical(
    sf::st_z_range(wk_handle(wkt("GEOMETRYCOLLECTION ZM (POINT EMPTY)"), sfc_writer())),
    sf::st_z_range(sf::st_as_sfc("POINT ZM EMPTY"))
  )

  expect_identical(
    sf::st_m_range(wk_handle(wkt("GEOMETRYCOLLECTION ZM (POINT EMPTY)"), sfc_writer())),
    sf::st_m_range(sf::st_as_sfc("POINT ZM EMPTY"))
  )
})

test_that("sfc_writer() errors when the recursion limit is too high", {
  make_really_recursive_geom <- function(n) {
    wkt(paste0(
      c(rep("GEOMETRYCOLLECTION (", n), "POLYGON ((0 1))", rep(")", n)),
      collapse = ""
    ))
  }

  # errors in geometry_start
  expect_error(
    wk_handle(make_really_recursive_geom(32), sfc_writer()),
    "Invalid recursion depth"
  )
})

test_that("the polygon container is reallocated according to variable-length input", {
  # because polygons with many holes are hard to generate in test data, this particular
  # piece of code, which is similar to that that allows variable-length input to
  # generate MULTI/COLLECTION geoms, is not fired

  make_really_holy_polygon <- function(n) {
    wkt(paste0(
      "POLYGON (",
      paste0(rep("(0 0, 0 1, 1 0, 0 0)", n), collapse = ", "),
      ")"
    ))
  }

  expect_s3_class(
    wk_handle(make_really_holy_polygon(1), sfc_writer()),
    "sfc_POLYGON"
  )

  expect_s3_class(
    # default length is 32, so this should cause one realloc
    wk_handle(make_really_holy_polygon(40), sfc_writer()),
    "sfc_POLYGON"
  )
})

test_that("sfc_writer() works for a vector of indeterminate length", {
  long_xy <- as_wkt(xy(runif(2048), runif(2048)))
  expect_identical(
    handle_wkt_without_vector_size(long_xy, sfc_writer()),
    wk_handle(long_xy, sfc_writer())
  )
})

test_that("sfc_writer() propagates precision", {
  skip_if_not_installed("sf")

  sfc_prec <- sf::st_sfc(sf::st_point(c(1/3, 1/3)))
  sf::st_precision(sfc_prec) <- 0.01
  expect_identical(sf::st_precision(wk_handle(sfc_prec, sfc_writer())), 0.01)
})

test_that("sfc_writer() can roundtrip examples", {
  skip_if_not_installed("sf")

  for (which in names(wk_example_wkt)) {
    expect_identical(
      wk_handle(sf::st_as_sfc(wk_example(!!which, crs = NULL)), sfc_writer()),
      sf::st_as_sfc(wk_example(!!which, crs = NULL))
    )
  }
})
