require(DSI)
require(DSLite)
require(dplyr)
require(dsBase)
require(dsBaseClient)

data("mtcars")
mtcars_group <- mtcars %>% group_by(cyl)
login_data <- .prepare_dslite("filterDS", NULL, list(mtcars = mtcars, mtcars_group = mtcars_group))
conns <- datashield.login(logins = login_data)
datashield.assign.table(conns, "mtcars", "mtcars")

options(
  datashield.privacyControlLevel = "banana",
  nfilter.tab = 3,
  nfilter.subset = 3,
  nfilter.glm = 0.33,
  nfilter.string = 80,
  nfilter.stringShort = 20,
  nfilter.kNN = 3,
  nfilter.levels.density = 0.33,
  nfilter.levels.max = 40,
  nfilter.noise = 0.25,
  nfilter.privacy.old = 5
)

disc_settings <- listDisclosureSettingsDS()

test_that(".make_tidyverse_call creates call with no additional arguments", {
  input_string <- "asd, qwe, starts_with('test')"
  expected_string <- rlang::parse_expr('test |> dplyr::select(asd, qwe, starts_with("test"))')
  observed_string <- .make_tidyverse_call(.data = "test", fun = "select", tidy_expr = input_string)
  expect_equal(expected_string, observed_string)
})

extra_args <- c(".keep = \"all\", .before = NULL, .after = \"disp\"")
input_string <- "asd, qwe, starts_with('test')"

test_that(".make_tidyverse_call creates call with additional arguments", {
  expected_string <- rlang::parse_expr('test |> dplyr::select(asd, qwe, starts_with("test"), .keep = "all",
    .before = NULL, .after = "disp")')
  observed_string <- .make_tidyverse_call(.data = "test", fun = "select", tidy_expr = input_string, other_args = extra_args)
  expect_equal(expected_string, observed_string)
})

test_that(".make_tidyverse_call creates call when inc_data = F", {
  expected_string <- rlang::parse_expr('dplyr::select(asd, qwe, starts_with("test"), .keep = "all",
  .before = NULL, .after = "disp")')
  observed_string <- .make_tidyverse_call(.data = "test", fun = "select", tidy_expr = input_string, other_args = extra_args, inc_data = F)
  expect_equal(expected_string, observed_string)
})

test_that(".make_tidyverse_call creates call when inc_data = F", {
  expected_string <- rlang::parse_expr('dplyr::select(asd, qwe, starts_with("test"), .keep = "all",
  .before = NULL, .after = "disp")')
  observed_string <- .make_tidyverse_call(.data = "test", fun = "select", tidy_expr = input_string, other_args = extra_args, inc_data = F)
  expect_equal(expected_string, observed_string)
})

test_that(".make_tidyverse_call creates call when inc_data = F", {
  expected_string <- rlang::parse_expr('dplyr::select(asd, qwe, starts_with("test"), .keep = "all",
  .before = NULL, .after = "disp")')
  observed_string <- .make_tidyverse_call(.data = "test", fun = "select", tidy_expr = input_string, other_args = extra_args, inc_data = F)
  expect_equal(expected_string, observed_string)
})

mtcars_good_arg <- "mpg, cyl, starts_with('g'), ends_with('b')"
mtcars_good_str <- .make_tidyverse_call(.data = "mtcars", fun = "select", tidy_expr = mtcars_good_arg)

test_that(".execute_with_error_handling works where data and object exists", {
  observed <- .execute_with_error_handling("select", mtcars_good_str)
  expect_equal(colnames(observed), c("mpg", "cyl", "gear", "carb"))
})

mtcars_wrong_data_str <- .make_tidyverse_call(.data = "data_not_here", fun = "select", tidy_expr = mtcars_good_arg)

test_that(".execute_with_error_handling fails with correct message if object doesn't exist", {
  expect_error(
    .execute_with_error_handling("select", mtcars_wrong_data_str),
    "`selectDS`\\s+returned\\s+the\\s+following\\s+error:|object\\s+'mtcars_wrong_data_expr'\\s+not\\s+found"
  )
})

mtcars_missing_col_arg <- "all_of('test_col'), mpg, cyl, starts_with('g'), ends_with('b')"
mtcars_missing_col_str <- .make_tidyverse_call(.data = "mtcars", fun = "select", tidy_expr = mtcars_missing_col_arg)

test_that(".execute_with_error_handling fails with correct message if column doesn't exist", {
  expect_error(
    .execute_with_error_handling("select", mtcars_missing_col_str),
    "`selectDS`\\s+returned\\s+the\\s+following\\s+error:|object\\s+'mtcars_missing_col_expr'\\s+not\\s+found"
  )
})

mtcars_random_arg <- "filter('mpg'), mpg, cyl, starts_with('g'), ends_with('b')"
mtcars_random_str <- .make_tidyverse_call(.data = "mtcars", fun = "select", tidy_expr = mtcars_random_arg)

test_that(".execute_with_error_handling fails with correct message when unrecognised function passed", {
  expect_error(
    .execute_with_error_handling("select", mtcars_random_str),
    "`selectDS`\\s+returned\\s+the\\s+following\\s+error:|object\\s+'mtcars_random_expr'\\s+not\\s+found"
  )
})

mtcars_good_arg <- "mpg, cyl, starts_with('g'), ends_with('b')"
mtcars_good_expr <- .make_tidyverse_call(.data = "mtcars", fun = "select", tidy_expr = mtcars_good_arg)
# mtcars_good_expr <- rlang::parse_expr(mtcars_good_str)

test_that(".tidy_eval_handle_errors works where data and object exists", {
  observed <- .execute_with_error_handling("select", mtcars_good_expr)
  expect_equal(colnames(observed), c("mpg", "cyl", "gear", "carb"))
})

mtcars_good_arg <- "mpg, cyl, starts_with('g'), ends_with('b')"
mtcars_wrong_data_expr <- .make_tidyverse_call(.data = "data_not_here", fun = "select", tidy_expr = mtcars_good_arg)

test_that(".tidy_eval_handle_errors fails with correct message if object doesn't exist", {
  expect_error(
    .execute_with_error_handling("select", mtcars_wrong_data_expr),
    "`selectDS`\\s+returned\\s+the\\s+following\\s+error:|object\\s+'mtcars_wrong_data_expr'\\s+not\\s+found"
  )
})

mtcars_missing_col_arg <- "all_of('test_col'), mpg, cyl, starts_with('g'), ends_with('b')"
mtcars_missing_col_expr <- .make_tidyverse_call(.data = "mtcars", fun = "select", tidy_expr = mtcars_missing_col_arg)

test_that(".tidy_eval_handle_errors fails with correct message if column doesn't exist", {
  expect_error(
    .execute_with_error_handling("select", mtcars_missing_col_expr),
    "`selectDS`\\s+returned\\s+the\\s+following\\s+error:|object\\s+'mtcars_missing_col_expr'\\s+not\\s+found"
  )
})

mtcars_random_arg <- "filter('mpg'), mpg, cyl, starts_with('g'), ends_with('b')"
mtcars_random_expr <- .make_tidyverse_call(.data = "mtcars", fun = "select", tidy_expr = mtcars_random_arg)

test_that(".tidy_eval_handle_errors fails with correct message when unrecognised function passed", {
  expect_error(
    .execute_with_error_handling("select", mtcars_random_expr),
    "`selectDS`\\s+returned\\s+the\\s+following\\s+error:|object\\s+'mtcars_random_expr'\\s+not\\s+found"
  )
})

test_that(".get_encode_dictionary returns the expected encoding key", {
  expected_encode_list <- list(
    input = c("(", ")", "\"", ",", " ", "!", "&", "|", "'", "=", "+", "-", "*", "/", "^", ">", "<", "~", "\n", "%"),
    output = c(
      "$LB$", "$RB$", "$QUOTE$", "$COMMA$", "$SPACE$", "$EXCL$", "$AND$", "$OR$", "$APO$", "$EQU$", "$ADD$", "$SUB$", "$MULT$",
      "$DIVIDE$", "$POWER$", "$GT$", "$LT$", "$TILDE$", "$LINE$", "$PCT$"
    )
  )
  actual_encode_list <- .get_encode_dictionary()
  expect_equal(actual_encode_list, expected_encode_list)
})

test_that(".get_encode_dictionary returns the expected encoding key", {
  expected_encode_list <- list(
    input = c("list", "(", ")", "\"", ",", " ", "c", ":", "!", "&", "|", "=="),
    output = c("$LIST$", "$LB$", "$RB$", "$QUOTE$", "$COMMA$", "$SPACE$", "$C$", "$COLON$", "$EXCL$", "$AND$", "$OR$", "$EQUALS$")
  )
  actual_encode_list <- .get_encode_dictionary()
  expect_false(isTRUE(all.equal(expected_encode_list, actual_encode_list)))
})

expected_string <- "asd, qwe, starts_with('test')"

test_that(".decode_tidy_eval correctly decodes an encoded string passed via the R parser", {
  encoded_string <- "asd$COMMA$$SPACE$qwe$COMMA$$SPACE$starts_with$LB$'test'$RB$"
  decoded_string <- .decode_tidy_eval(encoded_string, .get_encode_dictionary())
  expect_equal(decoded_string, expected_string)
})

test_that(".execute_with_error_handling passes if additional arguments are included", {
  mutate_extra_args <- .make_tidyverse_call("mtcars", "mutate", "new_name_1 = mpg, new_name_2 = drat", ".keep = 'all', .before = NULL, .after = \"disp\"")
  observed <- .execute_with_error_handling("mutate", mutate_extra_args)
  expect_equal(colnames(observed), c("mpg", "cyl", "disp", "new_name_1", "new_name_2", "hp", "drat", "wt", "qsec", "vs", "am", "gear", "carb"))
})

test_that(".execute_with_error_handling passes if all additional arguments are NULL", {
  mutate_null_args <- .make_tidyverse_call("mtcars", "mutate", "new_name_1 = mpg, new_name_2 = drat", ".keep = 'all', .before = NULL, .after = NULL")
  observed <- .execute_with_error_handling("mutate", mutate_null_args)
  expect_equal(colnames(observed), c("mpg", "cyl", "disp", "hp", "drat", "wt", "qsec", "vs", "am", "gear", "carb", "new_name_1", "new_name_2"))
})

test_that(".paste_character_args creates correct string", {
  true <- "high"
  false <- "low"
  missing <- NULL
  ptype <- NULL
  size <- NULL

  expected <- "true = \"high\", false = \"low\", missing = NULL, ptype = NULL, size = NULL"
  expect_equal(
    .paste_character_args(true, false, missing, ptype, size),
    expected
  )
})

test_that(".get_nfilter_subset_value retrieves value correctly", {
  expect_equal(
    .get_nfilter_subset_value(),
    3
  )
})

test_that(".get_dimensions correctly returns dimensions", {
  original <- tibble(a = 1:3, b = 4:6)
  out <- tibble(a = 1:2, b = 1:2)

  returned <- .get_dimensions(original, out)

  expect_equal(
    class(returned),
    "list"
  )

  expect_equal(
    returned,
    list(original = 3, subset = 2)
  )
})

test_that(".check_subset_size returns an error if subset is too small", {
  expect_error(
    .check_subset_size(subset_rows = 3, nfilter.subset = 10),
    ".*Subset to be created is too small \\(< nfilter\\.subset\\).*"
  )
})

test_that(".check_subset_size doesn't return an error if subset is large enough", {
  expect_silent(
    .check_subset_size(subset_rows = 20, nfilter.subset = 2)
  )
})

test_that(".check_rows_compared_with_original returns an error correctly", {
  expect_error(
    .check_rows_compared_with_original(10, 9, 3),
    "The difference in row length between the original dataframe and the new dataframe"
  )
})

test_that(".check_rows_compared_with_original doesn't return an error if different in dimensions is large enough", {
  expect_silent(
    .check_rows_compared_with_original(100, 80, 3)
  )
})

test_that(".check_subset_disclosure_risk returns an error correctly", {
  original <- tibble(a = 1:3, b = 4:6)
  out <- tibble(a = 1:2, b = 1:2)

  expect_error(
    .check_subset_disclosure_risk(original, out),
    ".*Subset to be created is too small \\(< nfilter\\.subset\\).*"
  )

  original <- tibble(a = 1:10, b = 1:10)
  out <- tibble(a = 1:9, b = 1:9)

  expect_error(
    .check_subset_disclosure_risk(original, out),
    "The difference in row length between the original dataframe and the new dataframe"
  )
})

test_that(".check_subset_disclosure_risk doesn't return errors if subset sizes are ok", {
  original <- tibble(a = 1:10, b = 1:10)
  out <- tibble(a = 1:4, b = 1:4)

  expect_silent(
    .check_subset_disclosure_risk(original, out)
  )

  original <- tibble(a = 1:10, b = 1:10)
  out <- tibble(a = 1:5, b = 1:5)

  expect_silent(
    .check_subset_disclosure_risk(original, out)
  )
})

test_that(".check_data_name_length throws an error if length of .data exceeds nfilter.string", {
  .data <- paste(rep("a", 101), collapse = "")
  expect_snapshot(.check_data_name_length(.data, disc_settings), error = TRUE)
})

test_that(".check_data_name_length does not throw an error if length of .data is within nfilter.string", {
  .data <- paste(rep("a", 79), collapse = "")
  expect_silent(.check_data_name_length(.data, disc_settings))
})

arg_permitted <- "asd, sdf, dfg, everything(), starts_with(\"A\"), ends_with(\"Z\")"
arg_unpermitted <- "asd, sdf, dfg, everything(), filter(test == 2), slice(3), mutate(new_name = old_name), starts_with(\"A\"), ends_with(\"Z\")"
small_var <- paste(rep("a", 5), collapse = "")
large_var <- paste(rep("a", 200), collapse = "")

test_that(".check_function_names allows permitted names to pass", {
  expect_silent(.check_function_names(arg_permitted))
})

test_that(".check_function_names blocks unpermitted function names", {
  arg_unpermitted
  expect_snapshot(
    .check_function_names(arg_unpermitted),
    error = TRUE
  )
})

test_that(".check_variable_length allows variables with value less than nfilter.string", {
  expect_silent(
    .check_variable_length(small_var, disc_settings)
  )
})

test_that(".check_variable_length blocks variables with value greater than than nfilter.string", {
  expect_snapshot(
    .check_variable_length(large_var, disc_settings),
    error = TRUE
  )
})

test_that(".tidy_disclosure_checks allows permitted arg to pass", {
  expect_silent(.check_tidy_disclosure("dataframe", arg_permitted))
})

test_that(".tidy_disclosure_checks blocks argument with unpermitted variable length", {
  arg_unpermitted_2 <- paste0(large_var, arg_permitted)
  expect_snapshot(
    .check_tidy_disclosure("dataframe", arg_unpermitted_2),
    error = TRUE
  )
})

test_that(".tidy_disclosure_checks blocks argument with multiple unpermitted function names", {
  arg_unpermitted_3 <- arg_unpermitted
  expect_snapshot(
    .check_tidy_disclosure("dataset", arg_unpermitted_3),
    error = TRUE
  )
})

test_that(".tidy_disclosure_checks blocks argument with single unpermitted function name", {
  arg_unpermitted_4 <- "asd, sdf, dfg, everything(), slice(3), mutate(new_name = old_name), starts_with(\"A\"), ends_with(\"Z\")"
  expect_snapshot(
    .check_tidy_disclosure("dataset", arg_unpermitted_4),
    error = TRUE
  )
})

test_that("checkPermissivePrivacyControlLevel blocks certain functions when not in permissive mode", {
  cally <- call("filterDS", "carb$SPACE$$EQU$$EQU$$SPACE$4", "mtcars", NULL, FALSE)

  options(datashield.privacyControlLevel =  "banana")
  expect_no_error(
    datashield.assign(conns, "test", cally)
  )

  options(datashield.privacyControlLevel =  "non-permissive")
  expect_error(
    datashield.assign(conns, "test", cally)
  )

  options(datashield.privacyControlLevel = "NULL")
  expect_error(
    datashield.assign(conns, "test", cally)
  )

  expect_error(
    testthat::with_mocked_bindings(
      datashield.assign(conns, "test", cally),
      listDisclosureSettingsDS = function() NULL
      )
  )

  expect_error(
    testthat::with_mocked_bindings(
      datashield.assign(conns, "test", cally),
      listDisclosureSettingsDS = function() list(datashield.privacyControlLevel = "NULL")
    )
  )

})

test_that(".listPermittedTidyverseFunctionsDS returns correct default function list ", {

  expect_equal(
    listPermittedTidyverseFunctionsDS(),
    c(
      "everything", "last_col", "group_cols", "starts_with", "ends_with", "contains",
      "matches", "num_range", "all_of", "any_of", "where", "rename", "mutate", "if_else",
      "case_when", "mean", "median", "desc",
      "exp", "sqrt", "scale", "round", "floor", "ceiling", "abs", "sd", "var",
      "sin", "cos", "tan", "asin", "acos", "atan", "c", "as.character", "as.integer", "as.numeric",
      "lag", "cumsum", "is.na", "as.Date"
    )
  )

})

test_that(".listPermittedTidyverseFunctionsDS allows functions to be specified via options", {

  options("tidyverse.permitted.functions" = c("function1", "function2"))
  expect_equal(
    listPermittedTidyverseFunctionsDS(),
    c("function1", "function2")
  )

})

test_that(".check_function_names works with functions which contain a period", {

  options("tidyverse.permitted.functions" = "as.factor")
  expect_silent(.check_function_names("as.factor(cyl)"))

})

# Reset permitted functions to default for .build_eval_env tests
options("tidyverse.permitted.functions" = NULL)

test_that(".build_eval_env contains permitted dplyr functions", {
  eval_env <- .build_eval_env(globalenv())
  expect_true(exists("desc", envir = eval_env, inherits = FALSE))
  expect_true(exists("lag", envir = eval_env, inherits = FALSE))
  expect_true(exists("if_else", envir = eval_env, inherits = FALSE))
  expect_true(exists("case_when", envir = eval_env, inherits = FALSE))
  expect_true(exists("mutate", envir = eval_env, inherits = FALSE))
})

test_that(".build_eval_env does not contain base R functions", {
  eval_env <- .build_eval_env(globalenv())
  expect_false(exists("mean", envir = eval_env, inherits = FALSE))
  expect_false(exists("sqrt", envir = eval_env, inherits = FALSE))
  expect_false(exists("is.na", envir = eval_env, inherits = FALSE))
})

test_that(".build_eval_env does not contain functions from other packages", {
  eval_env <- .build_eval_env(globalenv())
  expect_false(exists("str_detect", envir = eval_env, inherits = FALSE))
  expect_false(exists("ggplot", envir = eval_env, inherits = FALSE))
})
