use crate::error::{Error, ErrorLocation};

pub(crate) fn format_source_location_context(source: &str, loc: &ErrorLocation) -> String {
  let lines: Vec<&str> = source.lines().collect();
  let line_idx = loc.line as usize;
  let display_line = line_idx + 1;
  let display_column = if loc.column == 0 { 1 } else { loc.column as usize };

  let mut output = format!("css location: line {display_line}, column {display_column}");

  if lines.is_empty() {
    output.push_str("\n  (source is empty)");
    return output;
  }

  if line_idx >= lines.len() {
    output.push_str(&format!(
      "\n  (line is out of range; source has {} line(s))",
      lines.len()
    ));
    return output;
  }

  let start = line_idx.saturating_sub(1);
  let end = usize::min(line_idx + 1, lines.len() - 1);
  output.push_str("\ncontext:");

  for i in start..=end {
    let line = lines[i];
    output.push_str(&format!("\n{:>6} | {}", i + 1, line));
    if i == line_idx {
      let caret_pos = display_column.saturating_sub(1);
      let line_char_count = line.chars().count();
      let mut marker = String::with_capacity(caret_pos.max(line_char_count));
      for ch in line.chars().take(caret_pos) {
        marker.push(if ch == '\t' { '\t' } else { ' ' });
      }
      if caret_pos > line_char_count {
        marker.push_str(&" ".repeat(caret_pos - line_char_count));
      }
      output.push_str(&format!("\n       | {}^", marker));
    }
  }

  output
}

#[track_caller]
pub(crate) fn panic_with_test_error<T: std::fmt::Debug + std::fmt::Display>(
  helper: &str,
  stage: &str,
  source: &str,
  error: Error<T>,
) -> ! {
  let caller = std::panic::Location::caller();
  let location = match error.loc.as_ref() {
    Some(loc) => format_source_location_context(source, loc),
    None => "css location: <none>".to_string(),
  };
  panic!(
    "{helper}: {stage} failed\ncaller: {}:{}\nerror: {error}\nerror(debug): {error:?}\n{location}\nsource:\n{source}",
    caller.file(),
    caller.line(),
  );
}
