test_that("it creates basic lints", {
  code <- "before   %+%   after"
  tmpfile <- withr::local_tempfile(lines = code)
  expr <- get_source_expressions(tmpfile)$expressions[[2L]]
  xml <- expr$full_xml_parsed_content
  node <- xml2::xml_find_first(xml, "//SPECIAL")

  l <- xml_nodes_to_lints(
    xml = node,
    source_expression = expr,
    lint_message = "lint_msg",
    type = "warning"
  )

  expect_s3_class(l, "lint")
  expect_identical(l$filename, tmpfile)
  expect_identical(l$line_number, 1L)
  expect_identical(l$column_number, as.integer(xml2::xml_attr(node, "col1")))
  expect_identical(l$type, "warning")
  expect_identical(l$message, "lint_msg")
  expect_identical(l$line, code)
  expect_identical(l$ranges, list(as.integer(c(xml2::xml_attr(node, "col1"), xml2::xml_attr(node, "col2")))))

  # location XPaths work
  l_before_col <- xml_nodes_to_lints(
    xml = node,
    source_expression = expr,
    lint_message = "lint_msg",
    column_number_xpath = "number(./preceding-sibling::*[1]/@col2 + 1)",
    range_start_xpath = "number(./preceding-sibling::*[1]/@col1)",
    range_end_xpath = "number(./following-sibling::*[1]/@col2)"
  )
  expect_identical(l_before_col$column_number, nchar("before") + 1L)
  expect_identical(l_before_col$ranges, list(c(1L, nchar(code))))

  # Vectorization works
  ll <- xml_nodes_to_lints(
    xml = xml2::xml_find_all(xml, "//expr"),
    source_expression = expr,
    lint_message = "lint_msg"
  )
  expect_s3_class(ll, "lints")
  expect_length(ll, 3L)
  expect_s3_class(ll[[1L]], "lint")

  # Also for plain lists of xml_nodes, usually obtained by c(nodeset_a, nodeset_b)
  ll_unclassed <- xml_nodes_to_lints(
    xml = unclass(xml2::xml_find_all(xml, "//expr")),
    source_expression = expr,
    lint_message = "lint_msg"
  )
  expect_s3_class(ll_unclassed, "lints")
  expect_length(ll_unclassed, 3L)
  expect_s3_class(ll_unclassed[[1L]], "lint")

  # lint_message as a vector
  ll_msgvec <- xml_nodes_to_lints(
    xml = xml2::xml_find_all(xml, "//SYMBOL"),
    source_expression = expr,
    lint_message = letters[1L:2L]
  )
  expect_identical(ll_msgvec[[1L]]$message, "a")
  expect_identical(ll_msgvec[[2L]]$message, "b")
})

test_that("it handles multi-line lints correctly", {
  code <- c("before %+%", "  after")
  tmpfile <- withr::local_tempfile(lines = code)
  expr <- get_source_expressions(tmpfile)$expressions[[2L]]
  xml <- expr$full_xml_parsed_content
  node <- xml2::xml_find_first(xml, "/exprlist/expr")

  l <- xml_nodes_to_lints(
    xml = node,
    source_expression = expr,
    lint_message = "lint_msg"
  )

  expect_s3_class(l, "lint")
  expect_identical(l$filename, tmpfile)
  expect_identical(l$line_number, 1L)
  expect_identical(l$column_number, as.integer(xml2::xml_attr(node, "col1")))
  expect_identical(l$type, "style")
  expect_identical(l$message, "lint_msg")
  expect_identical(l$line, code[[1L]])
  expect_identical(l$ranges, list(as.integer(c(xml2::xml_attr(node, "col1"), nchar(code[1L])))))
})

test_that("it doesn't produce invalid lints", {
  # Part of #1427
  code <- "before   %+%   after"
  tmpfile <- withr::local_tempfile(lines = code)
  expr <- get_source_expressions(tmpfile)$expressions[[2L]]
  xml <- expr$full_xml_parsed_content
  node <- xml2::xml_find_first(xml, "//SPECIAL")

  # We can produce invalid location xpaths by requiring any non-existent node
  xp_column_number <- "number(./preceding-sibling::*[1]/@col2 + 1)"
  xp_range_start <- "number(./preceding-sibling::*[1]/@col1)"
  xp_range_end <- "number(./following-sibling::*[1]/@col2)"
  xp_invalid <- "number(./DOES-NOT-EXIST/@col1)"

  expect_warning(
    {
      lints1 <- xml_nodes_to_lints(
        xml = node,
        source_expression = expr,
        lint_message = "lint_msg",
        column_number_xpath = xp_column_number,
        range_start_xpath = xp_invalid,
        range_end_xpath = xp_range_end
      )
    },
    rex::rex("Could not find range start for lint. Defaulting to start of line.")
  )

  expect_identical(lints1[["column_number"]], nchar("before") + 1L)
  expect_identical(lints1[["ranges"]], list(c(1L, nchar(code))))

  expect_warning(
    {
      lints2 <- xml_nodes_to_lints(
        xml = node,
        source_expression = expr,
        lint_message = "lint_msg",
        column_number_xpath = xp_column_number,
        range_start_xpath = xp_range_start,
        range_end_xpath = xp_invalid
      )
    },
    rex::rex("Could not find range end for lint. Defaulting to width 1.")
  )

  expect_identical(lints2[["column_number"]], nchar("before") + 1L)
  expect_identical(lints2[["ranges"]], list(c(1L, 1L)))

  expect_warning(
    {
      lints3 <- xml_nodes_to_lints(
        xml = node,
        source_expression = expr,
        lint_message = "lint_msg",
        column_number_xpath = xp_invalid,
        range_start_xpath = xp_range_start,
        range_end_xpath = xp_range_end
      )
    },
    rex::rex("Could not find location for lint. Defaulting to start of range.")
  )

  expect_identical(lints3[["column_number"]], 1L)
  expect_identical(lints3[["ranges"]], list(c(1L, nchar(code))))
})

test_that("erroneous input errors", {
  expect_error(xml_nodes_to_lints(1L), "Expected an xml_nodeset", fixed = TRUE)
})
