#' Serve a mock API from files
#'
#' In this context, HTTP requests attempt to load API response fixtures from
#' files. This allows test code to proceed evaluating code that expects
#' HTTP requests to return meaningful responses. Requests that do not have a
#' corresponding fixture file raise errors, like how [without_internet()]
#' does.
#'
#' `use_mock_api()` and `stop_mocking()` allow you to turn on/off request
#' mocking for more convenient use in an interactive session.
#'
#' Requests are translated to mock file paths according to several rules that
#' incorporate the request method, URL, query parameters, and body. See
#' [build_mock_url()] for details.
#'
#' File paths for API fixture files may be relative to the 'tests/testthat'
#' directory, i.e. relative to the .R test files themselves. This is the default
#' location for storing and retrieving mocks, but you can put them anywhere you
#' want as long as you set the appropriate location with [.mockPaths()].
#'
#' @param expr Code to run inside the mock context
#' @return `with_mock_api()` returns the result of `expr`. `use_mock_api()` and
#'  `stop_mocking()` return nothing.
#' @export
#' @examples
#' library(httr2)
#' with_mock_api({
#'   # There are no mocks recorded in this example, so catch this request with
#'   # expect_GET()
#'   expect_GET(
#'     request("https://cran.r-project.org") %>% req_perform(),
#'     "https://cran.r-project.org"
#'   )
#'   # For examples with mocks, see the tests and vignettes
#' })
with_mock_api <- function(expr) {
  use_mock_api()
  on.exit(stop_mocking())
  eval.parent(expr)
}

#' @rdname with_mock_api
#' @export
use_mock_api <- function() {
  options(httr2_mock = mock_request)
  # trace req_body_apply to close the file connection that it creates when the
  # body inherits form_file
  trace_httr2(
    "req_body_apply",
    exit = quote(if (exists("con")) close(con))
  )
  invisible()
}

#' @rdname with_mock_api
#' @export
stop_mocking <- function() {
  options(httr2_mock = NULL)
  untrace_httr2("req_body_apply")
  invisible()
}

mock_request <- function(req) {
  f <- build_mock_url(get_current_redactor()(req))
  mockfile <- find_mock_file(f)
  if (!is.null(mockfile)) {
    return(load_response(mockfile, req))
  }
  # Else: fail.
  # For ease of debugging if a file isn't found, include it in the
  # error that gets printed.
  req$mockfile <- f
  return(stop_request(req))
}

#' Go through mock paths to find the local mock file location
#'
#' @param file A file path, as generated by [build_mock_url()].
#' @return A path to a file that exists, or `NULL` if none found.
#' @keywords internal
#' @export
find_mock_file <- function(file) {
  for (path in .mockPaths()) {
    # Look for files of any .extension in the indicated directory,
    # be they .R, .json, ...
    mp <- file.path(path, file)
    if (file.exists(mp) && !dir.exists(mp) && grepl("\\.", basename(mp))) {
      # With write_disk() downloading, 'file' may reference a specific
      # file and include the extension .R-FILE. So if that file exists,
      # no need to search for it. Just return it.
      return(mp)
    }
    # Turn the basename into a regular expression that will match it (and
    # only it) with any .extension
    mockbasename <- paste0("^\\Q", basename(mp), "\\E.[[:alnum:]]*$")
    mockfiles <- dir(dirname(mp),
      pattern = mockbasename, all.files = TRUE,
      full.names = TRUE
    )
    # Remove directories
    mockfiles <- setdiff(mockfiles, list.dirs(dirname(mp), full.names = TRUE))
    if (length(mockfiles)) {
      # TODO: check for length > 1
      return(mockfiles[1])
    }
  }
  return(NULL)
}

#' @importFrom utils tail
#' @importFrom httr2 response
load_response <- function(file, req) {
  verbose_message("Reading ", normalizePath(file))
  ext <- tail(unlist(strsplit(file, ".", fixed = TRUE)), 1)
  if (ext == "R") {
    # It's a full "response". Source it, and if it is from httr, adapt it
    adapt_httr_response(source(file)$value)
  } else if (ext %in% names(EXT_TO_CONTENT_TYPE)) {
    response(
      url = req$url,
      method = get_request_method(req),
      headers = list(`Content-Type` = EXT_TO_CONTENT_TYPE[[ext]]),
      status_code = 200L,
      body = readBin(file, "raw", n = file.size(file))
    )
  } else if (ext == "204") {
    response(
      url = req$url,
      method = get_request_method(req),
      status_code = 204L
    )
  } else {
    stop("Unsupported mock file extension: ", ext, call. = FALSE)
  }
}

adapt_httr_response <- function(resp) {
  # Restore httr2 1.0.0 cache
  resp$cache <- new.env(parent = emptyenv())
  
  if (inherits(resp, "httr2_response")) {
    return(resp)
  }
  stopifnot(inherits(resp, "response"))

  class(resp) <- "httr2_response"
  class(resp$headers) <- "httr2_headers"
  resp$body <- resp$content
  resp$content <- resp$all_headers <- resp$cookies <- resp$times <- NULL
  resp
}
