patrick::with_parameters_test_that(
  "sprintf_linter skips allowed usages",
  {
    linter <- sprintf_linter()

    # NB: using paste0, not sprintf, to avoid escaping '%d' in sprint fmt=
    expect_no_lint(paste0(call_name, "('hello %d', 1)"), linter)
    expect_no_lint(paste0(call_name, "('hello %d', x)"), linter)
    expect_no_lint(paste0(call_name, "('hello %d', x + 1)"), linter)
    expect_no_lint(paste0(call_name, "('hello %d', f(x))"), linter)
    expect_no_lint(paste0(call_name, "('hello %1$s %1$s', x)"), linter)
    expect_no_lint(paste0(call_name, "('hello %1$s %1$s %2$d', x, y)"), linter)
    expect_no_lint(paste0(call_name, "('hello %1$s %1$s %2$d %3$s', x, y, 1.5)"), linter)
  },
  .test_name = c("sprintf", "gettextf"),
  call_name = c("sprintf", "gettextf")
)

patrick::with_parameters_test_that(
  "sprintf_linter blocks disallowed usages",
  {
    linter <- sprintf_linter()
    unused_arg_msg <- "one argument not used by format"

    expect_lint(paste0(call_name, "('hello', 1)"), "constant", linter)

    expect_lint(paste0(call_name, "('hello')"), "constant", linter)
    expect_lint(paste0(call_name, "('100%% automated')"), "constant", linter)
    expect_lint(paste0(call_name, "('100%%%% automated')"), "constant", linter)
    expect_lint(paste0(call_name, "('100%%%s')"), "too few", linter)
    expect_lint(paste0(call_name, "('100%%%%s', x)"), "constant", linter)

    expect_lint(
      paste0(call_name, "('hello %d', 'a')"),
      rex::rex("invalid format '%d'; use format %s for character objects"),
      linter
    )

    expect_lint(
      paste0(call_name, "('hello %d', 1.5)"),
      rex::rex("invalid format '%d'; use format %f, %e, %g or %a for numeric objects"),
      linter
    )

    expect_lint(
      paste0(call_name, "('hello %d',)"),
      rex::rex("argument is missing, with no default"),
      linter
    )

    expect_lint(paste0(call_name, "('hello %1$s %s', 'a', 'b')"), unused_arg_msg, linter)
    expect_lint(paste0(call_name, "('hello %1$s %1$s', x, y)"), unused_arg_msg, linter)

    expect_lint(
      paste0(call_name, "('hello %1$s %1$s %3$d', x, y)"),
      rex::rex("reference to non-existent argument 3"),
      linter
    )

    expect_lint(
      paste0(call_name, "('hello %1$s %1$s %2$d %3$d', x, y, 1.5)"),
      rex::rex("invalid format '%d'; use format %f, %e, %g or %a for numeric objects"),
      linter
    )
  },
  .test_name = c("sprintf", "gettextf"),
  call_name = c("sprintf", "gettextf")
)

test_that("edge cases are detected correctly", {
  linter <- sprintf_linter()

  # works with multi-line sprintf and comments
  expect_no_lint(
    trim_some("
      sprintf(
        'test fmt %s', # this is a comment
        2
      )
    "),
    linter
  )

  expect_no_lint(
    trim_some("
      'test fmt %s' |>   # this is a pipe comment
        sprintf(         # this is an opening comment
          2              # this is a mid-call comment
        )
    "),
    linter
  )

  expect_lint(
    trim_some("
      'test fmt' |>   # this is a pipe comment
        sprintf()
    "),
    "constant",
    linter
  )

  # dots
  expect_no_lint("sprintf('%d %d, %d', id, ...)", linter)

  # TODO(#1265) extend ... detection to at least test for too many arguments.

  # named argument fmt
  expect_no_lint("sprintf(x, fmt = 'hello %1$s %1$s')", linter)

  expect_lint(
    "sprintf(x, fmt = 'hello %1$s %1$s %3$d', y)",
    list(message = rex::rex("reference to non-existent argument 3")),
    linter
  )

  # #2131: xml2lang stripped necessary whitespace
  expect_no_lint("sprintf('%s', if (A) '' else y)", linter)

  expect_no_lint("sprintf('100%%%s', x)", linter)
})

local({
  linter <- sprintf_linter()
  unused_fmt_msg <- "too few arguments"
  unused_arg_msg <- "one argument not used by format"
  pipes <- pipes(exclude = "%$%")
  patrick::with_parameters_test_that(
    "piping into sprintf works",
    {
      expect_no_lint(paste("x", pipe, "sprintf(fmt = '%s')"), linter)
      # no fmt= specified -> this is just 'sprintf("%s", "%s%s")', which won't lint
      expect_no_lint(paste('"%s"', pipe, 'sprintf("%s%s")'), linter)
      expect_lint(paste("x", pipe, "sprintf(fmt = '%s%s')"), unused_fmt_msg, linter)

      # Cannot evaluate statically --> skip
      expect_no_lint(paste("x", pipe, 'sprintf("a")'), linter)
      # Nested pipes
      expect_lint(
        paste("'%%sb'", pipe, "sprintf('%s')", pipe, "sprintf('a')"),
        list(column_number = nchar(paste("'%%sb'", pipe, "x")), message = "constant"),
        linter
      )
      expect_lint(
        paste("x", pipe, 'sprintf(fmt = "%s")', pipe, 'sprintf(fmt = "%s%s")'),
        list(column_number = nchar(paste("x", pipe, 'sprintf(fmt = "%s")', pipe, "x")), message = unused_fmt_msg),
        linter
      )
      expect_lint(
        paste("x", pipe, 'sprintf(fmt = "%s%s")', pipe, 'sprintf(fmt = "%s")'),
        list(column_number = nchar(paste("x", pipe, "x")), message = unused_fmt_msg),
        linter
      )
    },
    pipe = pipes,
    .test_name = names(pipes)
  )
})

test_that("pipe logic survives adversarial comments", {
  linter <- sprintf_linter()

  expect_no_lint(
    trim_some("
      x |> # comment
      sprintf(fmt = '%s')
    "),
    linter
  )

  expect_no_lint(
    trim_some('
      "%s" %>% # comment
      sprintf("%s%s")
    '),
    linter
  )
})

test_that("lints vectorize", {
  expect_lint(
    trim_some("{
      sprintf('%s', a, b)
      sprintf('%s%s', a)
    }"),
    list(
      list("one argument not used by format", line_number = 2L),
      list("too few arguments", line_number = 3L)
    ),
    sprintf_linter()
  )
})
