#' Snapshot testing for R objects
#' @description
#' A flexible wrapper around [testthat::expect_snapshot_file()] that allows
#' snapshotting any R object, not just data.frames. This function provides
#' a convenient interface for snapshot testing with sensible defaults for
#' serialization.
#'
#' When using RDS format (the default), snapshots are compared using
#' [waldo::compare()] which provides rich, visual diffs in
#' [testthat::snapshot_review()]. This makes it much easier to review
#' changes to complex R objects.
#'
#' @param x An R object to snapshot. Can be any R object including lists,
#'   models, data.frames, vectors, etc.
#' @param name [character] snapshot name (file extension added
#'   automatically)
#' @param writer [function] Function to write the object to a file.
#'   Default is [save_rds()].
#'   Other options include [save_json()], [save_deparse()], [save_csv()].
#'   Custom writer functions should accept `x` and return a file path.
#' @inheritParams compare_file_object
#' @param tolerance If non-`NULL`, used as threshold for ignoring small
#'   floating point differences when comparing numeric vectors. Only applies
#'   when `writer` produces an RDS file (the default); silently ignored for
#'   text-based formats (JSON, CSV, deparse, etc.). See [waldo::compare()] for
#'   full details.
#' @inheritDotParams testthat::expect_snapshot_file -path -name -compare
#' @returns [NULL] (from [testthat::expect_snapshot_file()])
#' @export
#' @details
#' When using RDS format (the default), snapshots can vary across R versions
#' and platforms even with fixed serialization versions. Consider using
#' `variant = platform_variant()` for RDS snapshots to handle these
#' differences, or use text-based formats like JSON or deparse for more
#' stable snapshots across platforms and versions.
#'
#' The RDS comparison uses [waldo::compare()] internally, which provides
#' rich visual diffs in [testthat::snapshot_review()]. This is particularly
#' useful for complex objects like models, nested lists, or data structures
#' where byte-level comparison would be difficult to interpret.
#' @examples
#' \donttest{
#' # expect_snapshot_object() must be called inside a test_that() block with
#' # testthat 3rd edition active. Outside a test suite, the snapshot is
#' # skipped because there is no reference file to compare against.
#' withr::with_tempdir({
#'   testthat::test_that("snapshot examples", {
#'     testthat::local_edition(3)
#'
#'     # Snapshot a list (RDS format with platform/version variant)
#'     expect_snapshot_object(
#'       list(a = 1, b = 2), name = "config", variant = platform_variant()
#'     )
#'
#'     # Snapshot a model
#'     model <- lm(mpg ~ wt, data = mtcars)
#'     expect_snapshot_object(
#'       model, name = "model", variant = platform_variant()
#'     )
#'
#'     # Snapshot with JSON format (for human-readable diffs)
#'     # Text formats don't need variants
#'     expect_snapshot_object(iris[1:5, ], name = "iris", writer = save_json)
#'
#'     # Snapshot with deparse format
#'     expect_snapshot_object(
#'       list(x = 1:5), name = "simple_list", writer = save_deparse
#'     )
#'   })
#' })
#' }
expect_snapshot_object <- function(x,
                                   name,
                                   writer = save_rds,
                                   print = FALSE,
                                   tolerance = NULL,
                                   ...) {
  path <- writer(x)
  # Get file extension from the path returned by writer
  ext <- tools::file_ext(path)
  # Determine comparison method based on file extension
  # Text-based formats use line-by-line comparison
  # (ignores line-ending differences)
  # RDS files use object comparison with diffobj for better visualization
  # Binary formats use byte-by-byte comparison
  text_extensions <- c("txt", "json", "R", "csv", "md", "yml", "yaml", "xml")

  compare <- if (ext %in% text_extensions) {
    testthat::compare_file_text
  } else if (ext == "rds") {
    function(...) {
      compare_file_object(print = print, tolerance = tolerance, ...)
    }
  } else {
    testthat::compare_file_binary
  }



  testthat::expect_snapshot_file(
    path = path,
    name = paste0(name, ".", ext),
    compare = compare,
    ...
  )
}

#' Save an R object to an RDS file
#' @param x An R object to save
#' @returns [character] Path to the temporary RDS file
#' @keywords internal
#' @export
save_rds <- function(x) {
  path <- tempfile(fileext = ".rds")
  saveRDS(x, path, version = 2)
  path
}

#' Save an R object to a JSON file
#' @param x An R object to save
#' @returns [character] Path to the temporary JSON file
#' @keywords internal
#' @export
save_json <- function(x) {
  path <- tempfile(fileext = ".json")
  jsonlite::write_json(x, path, pretty = TRUE, auto_unbox = TRUE)
  path
}

#' Save an R object to a text file using deparse
#' @param x An R object to save
#' @returns [character] Path to the temporary text file
#' @keywords internal
#' @export
save_deparse <- function(x) {
  path <- tempfile(fileext = ".txt")
  writeLines(deparse(x), path)
  path
}
