// Formatting subcommands — formula, style, and merge — which surface the
// richer parts of the mbtexcel library (cell formulas, styles, merges) beyond
// plain text cells.

///|
/// Reads a boolean flag from the parsed arguments.
fn flag(m : @argparse.Matches, name : String) -> Bool {
  m.flags.get_or_default(name, false)
}

///|
/// Index of the first `:` in `s`, or `None`.
fn colon_index(s : String) -> Int32? {
  for i in 0.. Array[String] raise {
  match colon_index(target) {
    None => [target]
    Some(_) => {
      let range = @inspect.parse_range(target) catch {
        RangeTooLarge(count~, ..) =>
          raise CliError(
            "error: range \{target} covers \{count} cells; max is \{@inspect.MAX_RANGE_CELLS}",
          )
        err => raise err
      }
      range.cells()
    }
  }
}

///|
async fn run_formula(m : @argparse.Matches) -> Unit {
  let file = arg(m, "file")
  let sheet = arg(m, "sheet")
  let cell = arg(m, "cell")
  // Accept the formula with or without a leading '=' (stored without it).
  let raw = arg(m, "formula")
  let formula = if raw.has_prefix("=") { raw[1:].to_owned() } else { raw }
  let workbook = @lib.open_file(file)
  workbook.set_cell_formula(sheet, cell, formula)
  write_file(file, @lib.write(workbook))
  println("set \{sheet}!\{cell} = =\{formula}")
}

///|
async fn run_merge(m : @argparse.Matches) -> Unit {
  let file = arg(m, "file")
  let sheet = arg(m, "sheet")
  let range = arg(m, "range")
  let workbook = @lib.open_file(file)
  workbook.merge_cells(sheet, range)
  write_file(file, @lib.write(workbook))
  println("merged \{sheet}!\{range}")
}

///|
async fn run_style(m : @argparse.Matches) -> Unit {
  let file = arg(m, "file")
  let sheet = arg(m, "sheet")
  let target = arg(m, "range")
  let workbook = @lib.open_file(file)
  // Build a style from the provided flags/options; unset ones are left alone.
  let style = @batch.build_style(
    bold=flag(m, "bold"),
    italic=flag(m, "italic"),
    number_format=arg(m, "number-format"),
    fill=arg(m, "fill"),
    font_color=arg(m, "font-color"),
    align=arg(m, "align"),
  )
  let style_id = workbook.new_style(style)
  let cells = expand_range(target)
  for cell in cells {
    workbook.set_cell_style(sheet, cell, style_id)
  }
  write_file(file, @lib.write(workbook))
  println("styled \{cells.length()} cell(s) in \{sheet}!\{target}")
}

// Tests for the range-expansion helper used by the `style` command.

///|
test "expand_range: a single cell is just itself" {
  inspect(expand_range("A1").join(","), content="A1")
  inspect(expand_range("C7").join(","), content="C7")
}

///|
test "expand_range: an A1:B2 range expands row-major" {
  inspect(expand_range("A1:B2").join(","), content="A1,B1,A2,B2")
  inspect(expand_range("A1:C1").join(","), content="A1,B1,C1")
}

///|
test "expand_range: reversed endpoints normalize" {
  inspect(expand_range("B2:A1").join(","), content="A1,B1,A2,B2")
}

///|
test "expand_range: rejects an over-large range" {
  try expand_range("A1:Z100000") |> ignore catch {
    CliError(message) =>
      inspect(message.has_prefix("error: range"), content="true")
    _ => fail("expected a CliError")
  } noraise {
    _ => fail("expected an over-large range to raise")
  }
  // The maximum XLSX range: its cell count (~1.7e10) would overflow 32-bit
  // Int and wrap past the cap, so this must be rejected via the Int64 count.
  try expand_range("A1:XFD1048576") |> ignore catch {
    CliError(_) => ()
    _ => fail("expected a CliError")
  } noraise {
    _ => fail("expected the maximum range to raise, not overflow")
  }
}

///|
test "expand_range: single-cell passthrough is verbatim, even noncanonical" {
  // A bare cell name (no colon) is not parsed or normalized — `$A$01` and
  // lowercase names reach the library call exactly as typed, as before the
  // shared parser was introduced.
  inspect(expand_range("$A$01").join(","), content="$A$01")
  inspect(expand_range("a1").join(","), content="a1")
}

///|
test "expand_range: legacy error texts are pinned exactly" {
  // The oversized-range message must stay byte-identical to the pre-parser
  // wording (agents and cram docs match on it).
  try expand_range("A1:K9091") |> ignore catch {
    CliError(message) =>
      inspect(
        message,
        content="error: range A1:K9091 covers 100001 cells; max is 100000",
      )
    _ => fail("expected a CliError")
  } noraise {
    _ => fail("expected an over-large range to raise")
  }
  // A malformed range half fails with the strict parser's message.
  try expand_range("A1:NOPE") |> ignore catch {
    err => inspect(err, content="invalid cell reference 'A1:NOPE'")
  } noraise {
    _ => fail("expected a malformed range to raise")
  }
}