# Sheet tab state.  These are the only worksheet settings whose rules span the
# whole workbook, so most of these fixtures need more than one sheet.

one <- function() data.frame(x = 1L)

wb_xml <- function(sheets) {
  xlsx_part(write_tmp(sheets), "xl/workbook.xml", raw = TRUE)
}

# ── What reaches the file ─────────────────────────────────────────────────────

test_that("a hidden sheet is marked hidden in workbook.xml", {
  x <- wb_xml(list(A = xl_sheet(one()), B = xl_sheet(one(), view = xl_sheet_view(visible = FALSE))))
  expect_match(x, '<sheet name="B" sheetId="2" state="hidden"', fixed = TRUE)
  # the visible one is untouched
  expect_match(x, '<sheet name="A" sheetId="1" r:id="rId1"/>', fixed = TRUE)
})

test_that("active and first_tab reach workbookView", {
  x <- wb_xml(list(A = xl_sheet(one()),
                   B = xl_sheet(one(), view = xl_sheet_view(active = TRUE)),
                   C = xl_sheet(one(), view = xl_sheet_view(first_tab = TRUE))))
  expect_match(x, 'activeTab="1"', fixed = TRUE)   # 0-based: sheet B
  expect_match(x, 'firstSheet="2"', fixed = TRUE)  # 0-based: sheet C
})

test_that("selected marks the tab without making it active", {
  x <- xlsx_part(write_tmp(list(A = xl_sheet(one()),
                                B = xl_sheet(one(), view = xl_sheet_view(selected = TRUE)))),
                 "xl/worksheets/sheet2.xml", raw = TRUE)
  expect_match(x, 'tabSelected="1"', fixed = TRUE)
})

test_that("visible = TRUE is not the same as unset, and neither hides", {
  x <- wb_xml(list(A = xl_sheet(one(), view = xl_sheet_view(visible = TRUE)), B = xl_sheet(one())))
  expect_false(grepl("state=", x, fixed = TRUE))
})

test_that("sheet view settings leave the cells alone", {
  skip_if_not_installed("readxl")
  df <- data.frame(a = 1:3, stringsAsFactors = FALSE)
  p <- write_tmp(list(A = xl_sheet(df),
                      B = xl_sheet(df, view = xl_sheet_view(active = TRUE, first_tab = TRUE))))
  expect_equal(as.data.frame(readxl::read_xlsx(p, sheet = "B")), df)
})

# ── The cross-sheet rules ─────────────────────────────────────────────────────

test_that("hiding every sheet is refused", {
  expect_error(write_tmp(list(A = xl_sheet(one(), view = xl_sheet_view(visible = FALSE)),
                              B = xl_sheet(one(), view = xl_sheet_view(visible = FALSE)))),
               "at least one visible sheet")
})

test_that("a hidden sheet cannot also be active or selected", {
  expect_error(write_tmp(list(A = xl_sheet(one()),
                              B = xl_sheet(one(), view = xl_sheet_view(
                                visible = FALSE, active = TRUE)))),
               "hidden but also marked active/selected")
  expect_error(write_tmp(list(A = xl_sheet(one()),
                              B = xl_sheet(one(), view = xl_sheet_view(
                                visible = FALSE, selected = TRUE)))),
               "hidden but also marked active/selected")
})

test_that("only one sheet may be active", {
  expect_error(write_tmp(list(A = xl_sheet(one(), view = xl_sheet_view(active = TRUE)),
                              B = xl_sheet(one(), view = xl_sheet_view(active = TRUE)))),
               "only one sheet may be active")
})

test_that("the first sheet cannot be hidden unless another is activated", {
  # Excel opens on the first sheet, so hiding it with nothing else active
  # produces a workbook with no sheet to show
  expect_error(write_tmp(list(A = xl_sheet(one(), view = xl_sheet_view(visible = FALSE)),
                              B = xl_sheet(one()))),
               "cannot be hidden unless another sheet")
  # ... and it is allowed once another sheet is active
  x <- wb_xml(list(A = xl_sheet(one(), view = xl_sheet_view(visible = FALSE)),
                   B = xl_sheet(one(), view = xl_sheet_view(active = TRUE))))
  expect_match(x, '<sheet name="A" sheetId="1" state="hidden"', fixed = TRUE)
  expect_match(x, 'activeTab="1"', fixed = TRUE)
})

test_that("error messages name the offending sheet", {
  expect_error(write_tmp(list(Data = xl_sheet(one()),
                              Notes = xl_sheet(one(), view = xl_sheet_view(
                                visible = FALSE, active = TRUE)))),
               '"Notes"', fixed = TRUE)
  expect_error(write_tmp(list(First = xl_sheet(one(), view = xl_sheet_view(active = TRUE)),
                              Second = xl_sheet(one(), view = xl_sheet_view(active = TRUE)))),
               '"First" and "Second"', fixed = TRUE)
})

test_that("unnamed sheets are identified by position in errors", {
  expect_error(write_tmp(list(xl_sheet(one(), view = xl_sheet_view(active = TRUE)),
                              xl_sheet(one(), view = xl_sheet_view(active = TRUE)))),
               "sheets 1 and 2")
})

# ── Interaction with plain data frames ────────────────────────────────────────

test_that("plain data frames are treated as unset and never trip the rules", {
  # a bare data frame has no tab settings, so it must not be read as hidden
  expect_silent(write_tmp(list(A = one(), B = one())))
  # mixed with an xl_sheet
  expect_silent(write_tmp(list(
    A = one(), B = xl_sheet(one(), view = xl_sheet_view(active = TRUE)))))
  # a single plain data frame is not "every sheet hidden"
  expect_silent(write_tmp(one()))
})

test_that("the flags are validated as logicals", {
  expect_error(xl_sheet(one(), view = xl_sheet_view(active = "yes")), "active")
  expect_error(xl_sheet(one(), view = xl_sheet_view(visible = 1)), "visible")
  expect_error(xl_sheet(one(), view = xl_sheet_view(first_tab = c(TRUE, TRUE))), "first_tab")
})

# ── Zero display, direction, selection and scroll position ───────────────────

view_xml <- function(...) {
  xlsx_part(write_tmp(list(D = xl_sheet(data.frame(x = 1:5),
                                        view = xl_sheet_view(...)))),
            "xl/worksheets/sheet1.xml", raw = TRUE)
}

test_that("hide_zero and right_to_left reach sheetView", {
  expect_match(view_xml(hide_zero = TRUE), 'showZeros="0"', fixed = TRUE)
  expect_match(view_xml(right_to_left = TRUE), 'rightToLeft="1"', fixed = TRUE)
  # FALSE is not TRUE: nothing is written
  expect_false(grepl("showZeros", view_xml(hide_zero = FALSE), fixed = TRUE))
})

test_that("selection accepts a single cell and a range", {
  expect_match(view_xml(selection = "B2"), 'sqref="B2"', fixed = TRUE)
  x <- view_xml(selection = "B2:C4")
  expect_match(x, 'sqref="B2:C4"', fixed = TRUE)
  # the top-left corner becomes the active cell
  expect_match(x, 'activeCell="B2"', fixed = TRUE)
})

test_that("selection accepts a data-frame-relative spec", {
  df <- data.frame(a = 1:5, b = 1:5)
  x <- xlsx_part(write_tmp(list(D = xl_sheet(df, view = xl_sheet_view(selection = list(rows = 1:2,
                                                                 cols = "a"))))),
                 "xl/worksheets/sheet1.xml", raw = TRUE)
  # data rows 1:2 are sheet rows 2:3 once the header is counted
  expect_match(x, 'sqref="A2:A3"', fixed = TRUE)
})

test_that("an inverted selection is rejected by the shared range parser", {
  # Excel would read the corner order as naming the active cell; writexl gives
  # that up in exchange for one strict range parser, and says so rather than
  # silently normalising
  expect_error(write_tmp(list(D = xl_sheet(data.frame(x = 1:5),
                 view = xl_sheet_view(selection = "C4:B2")))),
               "inverted")
})

test_that("top_left sets the scrolled-to cell", {
  expect_match(view_xml(top_left = "A3"), 'topLeftCell="A3"', fixed = TRUE)
  expect_match(view_xml(top_left = "C10"), 'topLeftCell="C10"', fixed = TRUE)
})

test_that("selection and top_left are independent", {
  x <- view_xml(selection = "B2", top_left = "A3")
  expect_match(x, 'topLeftCell="A3"', fixed = TRUE)
  expect_match(x, 'sqref="B2"', fixed = TRUE)
})

test_that("the view scalars are validated", {
  expect_error(xl_sheet(data.frame(x = 1), view = xl_sheet_view(hide_zero = "yes")), "hide_zero")
  expect_error(xl_sheet(data.frame(x = 1),
                        view = xl_sheet_view(right_to_left = 1)),
               "right_to_left")
  expect_error(write_tmp(list(D = xl_sheet(data.frame(x = 1),
                 view = xl_sheet_view(selection = "not a range")))),
               "selection")
})

test_that("the view scalars leave the cells alone", {
  skip_if_not_installed("readxl")
  df <- data.frame(a = 1:5, stringsAsFactors = FALSE)
  p <- write_tmp(list(D = xl_sheet(df, view = xl_sheet_view(
    hide_zero = TRUE, right_to_left = TRUE, selection = "A2",
    top_left = "A2"))))
  expect_equal(as.data.frame(readxl::read_xlsx(p)), df)
})

# ── Split panes ───────────────────────────────────────────────────────────────
#
# worksheet_split_panes() positions the split by distance in row-height and
# column-width units, not by row and column, so the interesting part is the
# conversion.

# `split` is a view setting; everything else passed here is a sheet setting
# (row heights, column widths) that the split conversion has to account for.
pane <- function(split, ...) {
  sheet <- xl_sheet(data.frame(a = 1:5, b = 1:5, cc = 1:5),
                    view = xl_sheet_view(split = split), ...)
  x <- xlsx_part(write_tmp(list(D = sheet)),
                 "xl/worksheets/sheet1.xml", raw = TRUE)
  m <- regmatches(x, regexpr("<pane[^/]*/>", x))
  if (!length(m)) NA_character_ else m
}

pane_num <- function(p, attr) {
  m <- regmatches(p, regexec(sprintf('%s="([0-9.]+)"', attr), p))[[1L]]
  if (length(m) == 2L) as.numeric(m[2L]) else 0
}

test_that("a split is placed from a cell reference", {
  expect_match(pane(split = "B3"), "<pane ", fixed = TRUE)
  # a split at B3 is below one header row plus one data row, and right of one
  # column, so both distances are non-zero
  expect_gt(pane_num(pane(split = "B3"), "ySplit"), 0)
  expect_gt(pane_num(pane(split = "B3"), "xSplit"), 0)
})

test_that("a split on one axis only leaves the other unset", {
  expect_equal(pane_num(pane(split = "A3"), "xSplit"), 0)
  expect_gt(pane_num(pane(split = "A3"), "ySplit"), 0)
  expect_equal(pane_num(pane(split = "B1"), "ySplit"), 0)
  expect_gt(pane_num(pane(split = "B1"), "xSplit"), 0)
})

test_that("the split distance follows the sheet's real row heights", {
  # this is the whole point of converting rather than assuming 15 per row:
  # taller rows above the split must push it further down
  base <- pane_num(pane(split = "A3"), "ySplit")
  taller <- pane_num(pane(split = "A3", default_row_height = 30), "ySplit")
  tallest <- pane_num(pane(split = "A3", rows = xl_row_spec(1, height = 45)),
                      "ySplit")
  expect_gt(taller, base)
  expect_gt(tallest, taller)
})

test_that("the split distance follows the sheet's real column widths", {
  base <- pane_num(pane(split = "B1"), "xSplit")
  wider <- pane_num(pane(split = "B1", cols = xl_col_spec("a", width = 20)),
                    "xSplit")
  expect_gt(wider, base)
})

test_that("raw units can be given directly", {
  p <- pane(split = list(vertical = 15, horizontal = 8.43))
  expect_gt(pane_num(p, "ySplit"), 0)
  expect_gt(pane_num(p, "xSplit"), 0)
  # one axis only
  expect_equal(pane_num(pane(split = list(vertical = 15)), "xSplit"), 0)
})

test_that("raw split units are validated", {
  expect_error(write_tmp(list(D = xl_sheet(data.frame(x = 1),
                 view = xl_sheet_view(split = list(vertical = -1))))),
               "non-negative")
  expect_error(write_tmp(list(D = xl_sheet(data.frame(x = 1),
                 view = xl_sheet_view(split = list(sideways = 1))))),
               "unknown `split`")
})

test_that("split and freeze cannot both be set", {
  expect_error(write_tmp(list(D = xl_sheet(data.frame(x = 1:5), freeze = "B2",
                 view = xl_sheet_view(split = "B2")))),
               "cannot both be set")
  # either alone is fine
  expect_silent(write_tmp(list(D = xl_sheet(data.frame(x = 1:5),
                  view = xl_sheet_view(split = "B2")))))
  expect_silent(write_tmp(list(D = xl_sheet(data.frame(x = 1:5), freeze = "B2"))))
})

test_that("a split leaves the cells alone", {
  skip_if_not_installed("readxl")
  df <- data.frame(a = 1:5, stringsAsFactors = FALSE)
  p <- write_tmp(list(D = xl_sheet(df, view = xl_sheet_view(split = "B3"))))
  expect_equal(as.data.frame(readxl::read_xlsx(p)), df)
})

test_that("the guards on an empty or malformed view hold", {
  expect_null(.resolve_sheet_visibility(list()))
  expect_null(.sheet_view_of(data.frame(a = 1)))
  bad <- xl_sheet(data.frame(a = 1))
  bad$view <- list(active = TRUE)
  expect_error(.sheet_view_of(bad), "must be an xl_sheet_view object")
  expect_error(write_tmp(list(D = bad)), "must be an xl_sheet_view object")
})

test_that("a raw split spec names its own unknown elements", {
  expect_error(write_tmp(list(D = xl_sheet(data.frame(a = 1:3),
    view = xl_sheet_view(split = list(vertical = 10, sideways = 2))))),
    "unknown `split` element(s): sideways", fixed = TRUE)
})
