
test_that("library permissions are validated before restore", {
  skip_on_os("windows")
  inaccessible <- renv_scope_tempfile()
  dir.create(inaccessible, mode = "0100")
  expect_false(renv_install_preflight_permissions(inaccessible))
})

test_that("restore() gives an error if no lockfile exists", {
  renv_tests_scope()
  expect_false(file.exists("renv.lock"))
  expect_error(restore())
})

test_that("we can restore packages after init", {
  skip_on_cran()
  renv_tests_scope("breakfast")

  init()

  libpath <- renv_paths_library()
  before <- list.files(libpath)

  unlink(renv_paths_library(), recursive = TRUE)
  restore()

  after <- list.files(libpath)
  expect_setequal(before, after)

})

test_that("restore can recover when required packages are missing", {
  skip_on_cran()
  renv_tests_scope("breakfast")
  init()

  remove("oatmeal")
  snapshot(force = TRUE)
  unlink(renv_paths_library(), recursive = TRUE)
  restore()

  expect_true(renv_package_installed("oatmeal"))

})

test_that("restore(clean = TRUE) removes packages not in the lockfile", {

  renv_tests_scope("oatmeal")
  init()

  renv_scope_options(renv.config.auto.snapshot = FALSE)
  install("bread")
  expect_true(renv_package_installed("bread"))

  restore(clean = TRUE)
  expect_false(renv_package_installed("bread"))

})

test_that("renv.records can be used to override records during restore", {

  renv_tests_scope("bread")
  init()

  install("bread@0.1.0")
  snapshot()
  expect_equal(renv_package_version("bread"), "0.1.0")

  bread <- list(Package = "bread", Version = "1.0.0", Source = "CRAN")
  overrides <- list(bread = bread)
  renv_scope_options(renv.records = overrides)

  restore()
  expect_equal(renv_package_version("bread"), "1.0.0")

})

test_that("install.staged works as expected", {

  renv_tests_scope("breakfast")

  init()
  library <- renv_paths_library(project = getwd())

  install.opts <- list(breakfast = "--version")

  local({

    renv_scope_options(
      renv.config.install.staged = TRUE,
      renv.config.install.transactional = TRUE,
      install.opts = install.opts
    )

    renv_scope_envvars(RENV_PATHS_CACHE = renv_scope_tempfile())

    unlink(renv_paths_library(), recursive = TRUE)
    expect_error(restore())
    files <- list.files(library)
    expect_true(length(files) == 0L)

  })

  local({

    renv_scope_options(
      renv.config.install.staged = FALSE,
      renv.config.install.transactional = FALSE,
      install.opts = install.opts
    )

    renv_scope_envvars(RENV_PATHS_CACHE = renv_scope_tempfile())

    unlink(renv_paths_library(), recursive = TRUE)
    expect_error(restore())
    files <- list.files(library)
    expect_true(length(files) != 0L)

  })

})

test_that("restore(lockfile = '/path/to/lockfile') works", {

  renv_tests_scope("bread")

  init()

  unlink(paths$library(), recursive = TRUE)
  restore(lockfile = "renv.lock")
  expect_true(renv_package_installed("bread"))

  unlink(paths$library(), recursive = TRUE)
  lockfile <- renv_lockfile_load(project = getwd())
  restore(lockfile = "renv.lock")
  expect_true(renv_package_installed("bread"))

})

test_that("restore(packages = <...>) works", {
  renv_tests_scope("breakfast")
  init()
  unlink(paths$library(), recursive = TRUE)
  restore(packages = "toast")
  expect_length(list.files(paths$library()), 2L)
  expect_true(renv_package_installed("bread"))
  expect_true(renv_package_installed("toast"))
})

test_that("restore ignores packages of incompatible architecture", {

  renv_tests_scope(c("unixonly", "windowsonly"))
  init()

  if (renv_platform_unix()) {

    expect_true(renv_package_installed("unixonly"))
    expect_false(renv_package_installed("windowsonly"))

    lockfile <- renv_lockfile_read("renv.lock")
    package <- lockfile$Packages$unixonly
    expect_identical(package$OS_type, "unix")

    remove("unixonly")
    restore()
    expect_true(renv_package_installed("unixonly"))

  } else {

    expect_true(renv_package_installed("windowsonly"))
    expect_false(renv_package_installed("unixonly"))

    lockfile <- renv_lockfile_read("renv.lock")
    package <- lockfile$Packages$windowsonly
    expect_identical(package$OS_type, "windows")

    remove("windowsonly")
    restore()
    expect_true(renv_package_installed("windowsonly"))

  }

})

test_that("restore handled records without version set", {

  renv_tests_scope()

  # create dummy lockfile
  snapshot()

  # read lockfile and add record without version
  lockfile <- renv_lockfile_load(project = getwd())
  lockfile$Packages$bread <- list(Package = "bread", Source = "Repository")
  renv_lockfile_save(lockfile, project = getwd())

  # try to restore
  restore()

  # check for success
  expect_true(renv_package_installed("bread"))
  expect_equal(renv_package_version("bread"), "1.0.0")

})

test_that("restore doesn't re-use active library paths", {

  renv_tests_scope()
  renv_scope_options(renv.settings.snapshot.type = "all")

  lib1 <- renv_scope_tempfile("lib1")
  lib2 <- renv_scope_tempfile("lib2")
  ensure_directory(c(lib1, lib2))
  .libPaths(c(lib2, .libPaths()))

  install("bread", library = lib2)
  expect_true(renv_package_installed("bread", lib.loc = lib2))

  lockfile <- snapshot(library = lib2, lockfile = NULL)
  restore(library = lib1, lockfile = lockfile)
  expect_true(renv_package_installed("bread", lib.loc = lib1))

})

test_that("restore(exclude = <...>) excludes as expected", {

  renv_tests_scope("breakfast")
  init()

  remove(c("bread", "breakfast", "oatmeal", "toast"))
  restore(exclude = "breakfast")
  expect_false(renv_package_installed("breakfast"))

})

test_that("restore works with explicit Source", {

  renv_tests_scope("breakfast")
  init()

  renv_scope_envvars(
    RENV_PATHS_LOCAL = NULL,
    RENV_PATHS_CACHE = NULL
  )

  record <- list(
    Package = "skeleton",
    Version = "1.0.0",
    Source  = renv_tests_path("local/skeleton/skeleton_1.0.0.tar.gz")
  )

  renv_test_retrieve(record)

  lockfile <- renv_lockfile_init(project = getwd())
  lockfile$Packages <- list(skeleton = record)
  renv_lockfile_write(lockfile, file = "renv.lock")
  remove("skeleton")

  restore()

  expect_true(renv_package_installed("skeleton"))
  expect_true(renv_package_version("skeleton") == "1.0.0")

})

test_that("restore() restores packages with broken symlinks", {

  skip_on_cran()
  renv_scope_options(renv.settings.cache.enabled = TRUE)
  renv_tests_scope("breakfast")
  init()

  # check it's installed
  pkgpath <- renv_package_find("breakfast")
  expect_true(renv_file_exists(pkgpath))

  # break the cache
  record <- list(Package = "breakfast", Version = "1.0.0")
  cachepath <- renv_cache_find(record)
  unlink(cachepath, recursive = TRUE)

  # check that it's broken
  expect_true(renv_file_broken(pkgpath))

  # try to restore
  restore()

  # check that we're happy again
  expect_false(renv_file_broken(pkgpath))
  expect_true(renv_file_exists(pkgpath))
  expect_true(renv_package_installed("breakfast"))

})

test_that("restore() also installs packages with broken symlinks", {
  skip_on_cran()
  skip_on_os("windows")
  renv_scope_options(renv.settings.cache.enabled = TRUE)
  project <- renv_tests_scope("breakfast")
  init()

  pkgpaths <- list.files(
    path = renv_paths_library(project = project),
    full.names = TRUE
  )

  links <- Sys.readlink(pkgpaths)
  expect_true(all(nzchar(links)))

  unlink(links, recursive = TRUE)
  expect_false(renv_package_installed("breakfast"))

  restore()
  expect_true(renv_package_installed("breakfast"))
})

test_that("we can restore a package installed with a custom repository", {

  project <- renv_tests_scope(isolated = TRUE)
  init()

  url <- unname(getOption("repos"))
  local({
    renv_scope_options(repos = character())
    install("bread", repos = c(TEST = url), rebuild = TRUE)
  })

  dcf <- renv_description_read(package = "bread")
  expect_equal(!!dcf$RemoteRepos, !!url)
  expect_equal(!!dcf$RemoteReposName, "TEST")

  writeLines("library(bread)", con = "_deps.R")
  local({
    renv_scope_options(repos = character())
    snapshot()
  })

  suppressMessages(remove.packages("bread"))
  local({
    renv_scope_options(repos = character())
    restore(rebuild = TRUE)
  })

  expect_true(renv_package_installed("bread"))

})

test_that("restore uses custom repository URL from lockfile", {

  project <- renv_tests_scope(isolated = TRUE)
  init()

  # install bread from a named custom repo, with no global repos set
  url <- unname(getOption("repos"))
  local({
    renv_scope_options(repos = character())
    install("bread", repos = c(TEST = url))
  })

  # snapshot with no global repos
  writeLines("library(bread)", con = "_deps.R")
  local({
    renv_scope_options(repos = character())
    snapshot()
  })

  # check lockfile: no global repos, but package record has the custom URL
  lockfile <- renv_lockfile_read("renv.lock")
  expect_length(lockfile$R$Repositories, 0L)
  expect_equal(lockfile$Packages$bread$Repository, url)

  # remove the package and restore with no global repos
  suppressMessages(remove.packages("bread"))
  expect_false(renv_package_installed("bread"))

  local({
    renv_scope_options(repos = character())
    restore()
  })

  expect_true(renv_package_installed("bread"))

})

test_that("restore(strict = TRUE) uses only the recorded repository URL", {

  project <- renv_tests_scope(isolated = TRUE)
  init()

  # install bread from a named custom repo, with no global repos set
  url <- unname(getOption("repos"))
  local({
    renv_scope_options(repos = character())
    install("bread", repos = c(TEST = url))
  })

  # snapshot with no global repos
  writeLines("library(bread)", con = "_deps.R")
  local({
    renv_scope_options(repos = character())
    snapshot()
  })

  # verify lockfile has the custom URL
  lockfile <- renv_lockfile_read("renv.lock")
  expect_equal(lockfile$Packages$bread$Repository, url)

  # remove the package
  suppressMessages(remove.packages("bread"))
  expect_false(renv_package_installed("bread"))

  # strict restore with the correct URL should succeed
  local({
    renv_scope_options(repos = character())
    restore(strict = TRUE)
  })
  expect_true(renv_package_installed("bread"))

  # now simulate a bad repository URL in the lockfile;
  # disable the cache so we can verify the download path fails
  suppressMessages(remove.packages("bread"))
  lockfile$Packages$bread$Repository <- "file:///nonexistent/repo"
  renv_lockfile_write(lockfile, "renv.lock")

  # strict restore with a bad URL should fail
  local({
    renv_scope_options(repos = character(), renv.config.cache.enabled = FALSE)
    expect_error(restore(strict = TRUE))
  })
  expect_false(renv_package_installed("bread"))

})

test_that("restore(strict = FALSE) falls back when the recorded URL is bad", {

  renv_tests_scope("bread")
  init()

  # doctor the lockfile with a bogus URL-valued Repository
  lockfile <- renv_lockfile_read("renv.lock")
  lockfile$Packages$bread$Repository <- "file:///nonexistent/repo"
  renv_lockfile_write(lockfile, "renv.lock")

  # remove the package and restore with the real repo still available;
  # strict = FALSE (default) should fall back and succeed
  remove("bread")
  expect_false(renv_package_installed("bread"))

  restore()
  expect_true(renv_package_installed("bread"))

})

test_that("restore(strict = TRUE) does not affect name-valued Repository fields", {

  renv_tests_scope("bread")
  init()

  # bread was installed from the test repo; its lockfile record should have
  # Repository = "CRAN" (a name, not a URL)
  lockfile <- renv_lockfile_read("renv.lock")
  expect_equal(lockfile$Packages$bread$Repository, "CRAN")

  # remove and strict-restore; name-valued Repository should resolve
  # through the configured repos as usual
  remove("bread")
  expect_false(renv_package_installed("bread"))

  restore(strict = TRUE)
  expect_true(renv_package_installed("bread"))

})

test_that("restore(strict = TRUE) resolves dependencies from standard repos", {

  project <- renv_tests_scope(isolated = TRUE)
  init()

  # install toast (depends on bread) from a custom-named repo
  url <- unname(getOption("repos"))
  local({
    renv_scope_options(repos = character())
    install("toast", repos = c(TEST = url))
  })

  # snapshot with no global repos
  writeLines("library(toast)", con = "_deps.R")
  local({
    renv_scope_options(repos = character())
    snapshot()
  })

  # verify toast has a URL-valued Repository; bread should too
  lockfile <- renv_lockfile_read("renv.lock")
  expect_equal(lockfile$Packages$toast$Repository, url)
  expect_equal(lockfile$Packages$bread$Repository, url)

  # remove both packages and strict-restore;
  # both should be retrieved from the recorded URL
  remove(c("toast", "bread"))
  expect_false(renv_package_installed("toast"))
  expect_false(renv_package_installed("bread"))

  local({
    renv_scope_options(repos = character())
    restore(strict = TRUE)
  })

  expect_true(renv_package_installed("toast"))
  expect_true(renv_package_installed("bread"))

})

test_that("restore(strict = TRUE) still works when repos.override is set", {

  skip_on_cran()
  skip_on_windows()

  renv_tests_scope("bread")
  init()

  # doctor the lockfile with a bogus URL-valued Repository
  lockfile <- renv_lockfile_read("renv.lock")
  lockfile$Packages$bread$Repository <- "file:///nonexistent/repo"
  renv_lockfile_write(lockfile, "renv.lock")

  # remove the package
  remove("bread")
  expect_false(renv_package_installed("bread"))

  # validate current behavior: repos.override nullifies the Repository
  # field before strict sees it, so restore succeeds
  renv_scope_options(renv.config.repos.override = getOption("repos"))
  restore(packages = "bread", strict = TRUE, rebuild = TRUE)
  expect_true(renv_package_installed("bread"))

})

test_that("the Repository field in a lockfile can be overridden", {

  skip_on_cran()
  skip_on_windows()

  # initialize project
  project <- renv_tests_scope("bread")
  init()

  # simulate bread having an external / unexpected repository url
  lockfile <- renv_lockfile_read("renv.lock")
  lockfile$Packages$bread$Repository <- path.expand("~")
  renv_lockfile_write(lockfile, "renv.lock")

  # try to force a specific repository URL via override
  renv_scope_options(renv.config.repos.override = getOption("repos"))
  remove("bread")
  restore(packages = "bread", rebuild = TRUE)
  expect_true(renv_package_installed("bread"))

})

test_that("restore(packages = ...) installs missing transitive dependencies", {
  skip_on_cran()
  renv_tests_scope("breakfast")
  init()

  remove("bread")
  expect_false(renv_package_installed("bread"))
  expect_true(renv_package_installed("toast"))

  restore(packages = "toast")

  expect_true(renv_package_installed("bread"))
  expect_true(renv_package_installed("toast"))
})

test_that("restore(packages = ..., exclude = ...) excludes transitive deps", {
  skip_on_cran()
  renv_tests_scope("breakfast")
  init()

  remove("bread")
  expect_false(renv_package_installed("bread"))

  restore(packages = "toast", exclude = "bread")

  # bread should still be missing because it was excluded
  expect_false(renv_package_installed("bread"))
})

test_that("restore(packages = ...) is a no-op for already-installed transitive deps", {
  skip_on_cran()
  renv_tests_scope("breakfast")
  init()

  # everything is already installed after init(); restore should succeed
  # without error and not reinstall anything
  expect_true(renv_package_installed("bread"))
  expect_true(renv_package_installed("toast"))

  restore(packages = "toast")

  expect_true(renv_package_installed("bread"))
  expect_true(renv_package_installed("toast"))
})

test_that("restore(packages = ...) walks the full transitive closure", {
  skip_on_cran()
  renv_tests_scope("breakfast")
  init()

  # bread is two levels deep: breakfast -> toast -> bread
  remove("bread")
  expect_false(renv_package_installed("bread"))

  restore(packages = "breakfast")

  expect_true(renv_package_installed("bread"))
  expect_true(renv_package_installed("toast"))
  expect_true(renv_package_installed("breakfast"))
})

test_that("restore(packages = ...) works for a leaf package with no deps", {
  skip_on_cran()
  renv_tests_scope("breakfast")
  init()

  remove("oatmeal")
  expect_false(renv_package_installed("oatmeal"))

  restore(packages = "oatmeal")

  expect_true(renv_package_installed("oatmeal"))
})

test_that("restore(packages = ...) expands deps for multiple requested packages", {
  skip_on_cran()
  renv_tests_scope("breakfast")
  init()

  remove("bread")
  remove("oatmeal")
  expect_false(renv_package_installed("bread"))
  expect_false(renv_package_installed("oatmeal"))

  restore(packages = c("toast", "oatmeal"))

  expect_true(renv_package_installed("bread"))
  expect_true(renv_package_installed("toast"))
  expect_true(renv_package_installed("oatmeal"))
})

test_that("renv_restore_recover installs latest versions of failed packages", {
  skip_on_cran()
  renv_tests_scope("bread")
  init()

  # remove bread to simulate a failed first pass
  remove.packages("bread", lib = renv_libpaths_active())
  expect_false(renv_package_installed("bread"))

  # set up restore state as restore() would
  renv_scope_restore(
    project  = getwd(),
    library  = renv_libpaths_active(),
    records  = list(bread = list(Package = "bread", Version = "1.0.0", Source = "Repository")),
    packages = "bread"
  )

  # recovery should install latest bread
  recovered <- renv_restore_recover("bread", getwd())
  expect_true("bread" %in% names(recovered))
  expect_true(renv_package_installed("bread"))
})
