mod table_widget;

use self::table_widget::{TableWidget, TableWidgetState};
use super::super::{
    config::ExploreConfig,
    nu_common::{NuSpan, NuText, collect_input, lscolorize},
    pager::{
        Frame, Transition, ViewInfo,
        report::{Report, Severity},
    },
};
use super::{
    ElementInfo, Layout, View, ViewConfig,
    cursor::{CursorMoveHandler, Position, WindowCursor2D},
    util::{make_styled_string, nu_style_to_tui},
};
use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent};
use nu_color_config::{StyleComputer, TextStyle};
use nu_protocol::{
    Config, Record, Value,
    engine::{EngineState, Stack},
};
use ratatui::{layout::Rect, widgets::Block};

pub use self::table_widget::Orientation;

#[derive(Debug, Clone)]
pub struct RecordView {
    layer_stack: Vec<RecordLayer>,
    mode: UIMode,
    orientation: Orientation,
    cfg: ExploreConfig,
    auto_tail: bool, // Track if tail mode is active for auto-scroll
    previous_row_count: usize,
    page_size: usize,
}

impl RecordView {
    pub fn new(columns: Vec<String>, records: Vec<Vec<Value>>, cfg: ExploreConfig) -> Self {
        let row_count = records.len();
        Self {
            layer_stack: vec![RecordLayer::new(columns, records)],
            mode: UIMode::View,
            orientation: Orientation::Top,
            cfg,
            auto_tail: true, // Enable auto-tail by default
            previous_row_count: row_count,
            page_size: 0,
        }
    }

    pub fn tail(&mut self, width: u16, height: u16) {
        let page_size =
            estimate_page_size(Rect::new(0, 0, width, height), self.cfg.table.show_header);
        tail_data(self, page_size as usize);
        self.auto_tail = true; // Enable auto-tail mode
    }

    pub fn transpose(&mut self) {
        let layer = self.get_top_layer_mut();
        transpose_table(layer);

        layer.reset_cursor();
    }

    pub fn get_top_layer(&self) -> &RecordLayer {
        self.layer_stack
            .last()
            .expect("we guarantee that 1 entry is always in a list")
    }

    pub fn get_top_layer_mut(&mut self) -> &mut RecordLayer {
        self.layer_stack
            .last_mut()
            .expect("we guarantee that 1 entry is always in a list")
    }

    pub fn set_top_layer_orientation(&mut self, orientation: Orientation) {
        let layer = self.get_top_layer_mut();
        layer.orientation = orientation;
        layer.reset_cursor();
    }

    /// Get the current position of the cursor in the table as a whole
    pub fn get_cursor_position(&self) -> Position {
        let layer = self.get_top_layer();
        layer.cursor.position()
    }

    /// Get the current position of the cursor in the window being shown
    pub fn get_cursor_position_in_window(&self) -> Position {
        let layer = self.get_top_layer();
        layer.cursor.window_relative_position()
    }

    /// Get the origin of the window being shown. (0,0), top left corner.
    pub fn get_window_origin(&self) -> Position {
        let layer = self.get_top_layer();
        layer.cursor.window_origin()
    }

    pub fn set_cursor_mode(&mut self) {
        self.mode = UIMode::Cursor;
    }

    pub fn set_view_mode(&mut self) {
        self.mode = UIMode::View;
    }

    pub fn get_current_value(&self) -> &Value {
        let Position { row, column } = self.get_cursor_position();
        let layer = self.get_top_layer();

        let (row, column) = match layer.orientation {
            Orientation::Top => (row, column),
            Orientation::Left => (column, row),
        };

        // These should never happen as long as the cursor is working correctly
        assert!(row < layer.record_values.len(), "row out of bounds");
        assert!(column < layer.column_names.len(), "column out of bounds");

        &layer.record_values[row][column]
    }

    fn create_table_widget<'a>(&'a mut self, cfg: ViewConfig<'a>) -> TableWidget<'a> {
        let style = self.cfg.table;
        let style_computer = cfg.style_computer;
        let Position { row, column } = self.get_window_origin();

        let layer = self.get_top_layer_mut();
        if layer.record_text.is_none() {
            let mut data =
                convert_records_to_string(&layer.record_values, cfg.nu_config, cfg.style_computer);
            lscolorize(&layer.column_names, &mut data, cfg.cwd, cfg.lscolors);

            layer.record_text = Some(data);
        }

        let headers = &layer.column_names;
        let data = layer.record_text.as_ref().expect("always ok");

        TableWidget::new(
            headers,
            data,
            style_computer,
            row,
            column,
            style,
            layer.orientation,
        )
    }

    fn update_cursors(&mut self, rows: usize, columns: usize) {
        match self.get_top_layer().orientation {
            Orientation::Top => {
                let _ = self
                    .get_top_layer_mut()
                    .cursor
                    .set_window_size(rows, columns);
            }

            Orientation::Left => {
                let _ = self
                    .get_top_layer_mut()
                    .cursor
                    .set_window_size(rows, columns);
            }
        }
    }

    fn create_records_report(&self) -> Report {
        let layer = self.get_top_layer();
        let covered_percent = report_row_position(layer.cursor);
        let cursor = report_cursor_position(self.mode, layer.cursor);
        let message = layer.name.clone().unwrap_or_default();
        // note: maybe came up with a better short names? E/V/N?
        let mode = match self.mode {
            UIMode::Cursor => String::from("EDIT"),
            UIMode::View => String::from("VIEW"),
        };

        Report::new(message, Severity::Info, mode, cursor, covered_percent)
    }
}

impl View for RecordView {
    fn draw(&mut self, f: &mut Frame, area: Rect, cfg: ViewConfig<'_>, layout: &mut Layout) {
        let mut table_layout = TableWidgetState::default();
        let table = self.create_table_widget(cfg);
        f.render_stateful_widget(table, area, &mut table_layout);

        *layout = table_layout.layout;

        self.update_cursors(table_layout.count_rows, table_layout.count_columns);

        // Update page_size
        self.page_size = estimate_page_size(area, self.cfg.table.show_header) as usize;

        // Check for new rows and handle auto-tail
        let current_row_count = self.get_top_layer().record_values.len();
        if current_row_count > self.previous_row_count {
            // Invalidate record_text to force redraw
            self.get_top_layer_mut().record_text = None;
            // If auto_tail, scroll to bottom
            if self.auto_tail {
                let page_size = self.page_size;
                if current_row_count > page_size {
                    self.get_top_layer_mut()
                        .cursor
                        .set_window_start_position(current_row_count - page_size, 0);
                }
            }
        }
        self.previous_row_count = current_row_count;

        if self.mode == UIMode::Cursor {
            let Position { row, column } = self.get_cursor_position_in_window();
            let info = get_element_info(
                layout,
                row,
                column,
                table_layout.count_rows,
                self.get_top_layer().orientation,
                self.cfg.table.show_header,
            );

            if let Some(info) = info {
                highlight_selected_cell(f, info.clone(), &self.cfg);
            }
        }
    }

    fn handle_input(
        &mut self,
        _engine_state: &EngineState,
        _stack: &mut Stack,
        _layout: &Layout,
        info: &mut ViewInfo,
        key: KeyEvent,
    ) -> Transition {
        if key.code == KeyCode::PageUp {
            let page_size = self.page_size;
            let current_row = self.get_top_layer().cursor.window_origin().row;
            let new_row = current_row.saturating_sub(page_size);
            let layer = self.get_top_layer_mut();
            layer
                .cursor
                .set_window_start_position(new_row, layer.cursor.window_origin().column);
            let report = self.create_records_report();
            info.status = Some(report);
            return Transition::Ok;
        }
        if key.code == KeyCode::PageDown {
            let page_size = self.page_size;
            let current_row = self.get_top_layer().cursor.window_origin().row;
            let row_count = self.get_top_layer().record_values.len();
            let max_row = row_count.saturating_sub(page_size);
            let new_row = (current_row + page_size).min(max_row);
            let layer = self.get_top_layer_mut();
            layer
                .cursor
                .set_window_start_position(new_row, layer.cursor.window_origin().column);
            let report = self.create_records_report();
            info.status = Some(report);
            return Transition::Ok;
        }
        match self.handle_input_key(&key) {
            Ok((transition, ..)) => {
                if matches!(&transition, Transition::Ok | Transition::Cmd { .. }) {
                    let report = self.create_records_report();
                    info.status = Some(report);
                }

                transition
            }
            Err(e) => {
                log::error!("Error handling input in RecordView: {e}");
                let report = Report::message(e.to_string(), Severity::Err);
                info.status = Some(report);
                Transition::None
            }
        }
    }

    fn collect_data(&self) -> Vec<NuText> {
        let layer = self.get_top_layer();
        let mut texts = Vec::new();

        // Add headers
        for name in &layer.column_names {
            texts.push((name.clone(), TextStyle::default()));
        }

        // Add data
        for row in &layer.record_values {
            for value in row {
                let text = value.to_abbreviated_string(&Config::default());
                let text = strip_string(&text);
                texts.push((text, TextStyle::default()));
            }
        }

        texts
    }

    fn show_data(&mut self, pos: usize) -> bool {
        let layer = self.get_top_layer();
        let num_headers = layer.column_names.len();

        if pos < num_headers {
            // Header
            let column = pos;
            let row = 0;
            self.get_top_layer_mut()
                .cursor
                .set_window_start_position(row, column);
            return true;
        } else {
            let data_pos = pos - num_headers;
            let mut i = 0;
            for (data_row, cells) in layer.record_values.iter().enumerate() {
                if data_pos >= i && data_pos < i + cells.len() {
                    let column = data_pos - i;
                    self.get_top_layer_mut()
                        .cursor
                        .set_window_start_position(data_row, column);
                    return true;
                }
                i += cells.len();
            }
        }

        false
    }

    fn update(&mut self, _info: &mut ViewInfo) -> bool {
        false
    }

    fn exit(&mut self) -> Option<Value> {
        Some(build_last_value(self))
    }
}

fn build_last_value(v: &RecordView) -> Value {
    if v.mode == UIMode::Cursor {
        v.get_current_value().clone()
    } else if v.get_top_layer().count_rows() < 2 {
        build_table_as_record(v)
    } else {
        build_table_as_list(v)
    }
}

fn build_table_as_list(v: &RecordView) -> Value {
    let layer = v.get_top_layer();

    let vals = layer
        .record_values
        .iter()
        .map(|vals| {
            let record = layer
                .column_names
                .iter()
                .cloned()
                .zip(vals.iter().cloned())
                .collect();
            Value::record(record, NuSpan::unknown())
        })
        .collect();

    Value::list(vals, NuSpan::unknown())
}

fn build_table_as_record(v: &RecordView) -> Value {
    let layer = v.get_top_layer();

    let mut record = Record::new();
    if let Some(row) = layer.record_values.first() {
        record = layer
            .column_names
            .iter()
            .cloned()
            .zip(row.iter().cloned())
            .collect();
    }

    Value::record(record, NuSpan::unknown())
}

fn get_element_info(
    layout: &mut Layout,
    row: usize,
    column: usize,
    count_rows: usize,
    orientation: Orientation,
    with_head: bool,
) -> Option<&ElementInfo> {
    let with_head = with_head as usize;
    let index = match orientation {
        Orientation::Top => column * (count_rows + with_head) + row + 1,
        Orientation::Left => (column + with_head) * count_rows + row,
    };

    layout.data.get(index)
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum UIMode {
    Cursor,
    View,
}

#[derive(Debug, Clone)]
pub struct RecordLayer {
    pub column_names: Vec<String>,
    // These are the raw records in the current layer. The sole reason we keep this around is so we can return the original value
    // if it's being peeked. Otherwise we could accept an iterator over it.
    // or if it could be Cloneable we could do that anyway;
    // cause it would keep memory footprint lower while keep everything working
    // (yee would make return O(n); we would need to traverse iterator once again; but maybe worth it)
    pub record_values: Vec<Vec<Value>>,
    // This is the text representation of the record values (the actual text that will be displayed to users).
    // It's an Option because we need configuration to set it and we (currently) don't have access to configuration when things are created.
    pub record_text: Option<Vec<Vec<NuText>>>,
    orientation: Orientation,
    name: Option<String>,
    was_transposed: bool,
    pub cursor: WindowCursor2D,
}

impl RecordLayer {
    fn new(columns: Vec<String>, records: Vec<Vec<Value>>) -> Self {
        // TODO: refactor so this is fallible and returns a Result instead of panicking
        let cursor =
            WindowCursor2D::new(records.len(), columns.len()).expect("Failed to create cursor");

        let column_names = columns.iter().map(|s| strip_string(s)).collect();

        Self {
            column_names,
            record_values: records,
            record_text: None,
            cursor,
            orientation: Orientation::Top,
            name: None,
            was_transposed: false,
        }
    }

    fn set_name(&mut self, name: impl Into<String>) {
        self.name = Some(name.into());
    }

    fn count_rows(&self) -> usize {
        match self.orientation {
            Orientation::Top => self.record_values.len(),
            Orientation::Left => self.column_names.len(),
        }
    }

    fn count_columns(&self) -> usize {
        match self.orientation {
            Orientation::Top => self.column_names.len(),
            Orientation::Left => self.record_values.len(),
        }
    }

    fn get_column_header(&self) -> Option<String> {
        let col = self.cursor.column();
        self.column_names.get(col).map(|header| header.to_string())
    }

    fn reset_cursor(&mut self) {
        // TODO: refactor so this is fallible and returns a Result instead of panicking
        self.cursor = WindowCursor2D::new(self.count_rows(), self.count_columns())
            .expect("Failed to create cursor");
    }
}

impl CursorMoveHandler for RecordView {
    fn get_cursor(&mut self) -> &mut WindowCursor2D {
        &mut self.get_top_layer_mut().cursor
    }
    fn handle_enter(&mut self) -> Result<Transition> {
        match self.mode {
            UIMode::View => self.set_cursor_mode(),
            UIMode::Cursor => {
                let value = self.get_current_value();

                // ...but it only makes sense to drill down into a few types of values
                if !matches!(
                    value,
                    Value::Record { .. } | Value::List { .. } | Value::Custom { .. }
                ) {
                    return Ok(Transition::None);
                }

                let is_record = matches!(value, Value::Record { .. });
                let next_layer = create_layer(value.clone())?;
                push_layer(self, next_layer);

                if is_record {
                    self.set_top_layer_orientation(Orientation::Left);
                } else {
                    self.set_top_layer_orientation(self.orientation);
                }
            }
        }

        Ok(Transition::Ok)
    }
    fn handle_esc(&mut self) -> Transition {
        match self.mode {
            UIMode::View => {
                if self.layer_stack.len() > 1 {
                    self.layer_stack.pop();
                    self.mode = UIMode::Cursor;
                } else {
                    return Transition::Exit;
                }
            }
            UIMode::Cursor => self.set_view_mode(),
        }
        Transition::Ok
    }
    fn handle_expand(&mut self) -> Transition {
        Transition::Cmd(String::from("expand"))
    }
    fn handle_transpose(&mut self) -> Transition {
        match self.mode {
            UIMode::View => {
                self.transpose();

                Transition::Ok
            }
            _ => Transition::None,
        }
    }
    // for these, copy standard CursorMoveHandler for UIMode::View, but use special handling for UIMode::Cursor
    // NOTE: https://stackoverflow.com/a/31462293/2016290 says there's plans for Rust to allow calling super functions,
    // but not yet, and since they're all one line, it seems simpler to copy than make a lot of helper functions
    fn handle_left(&mut self) {
        match self.mode {
            UIMode::View => self.get_top_layer_mut().cursor.prev_column_i(),
            _ => self.get_top_layer_mut().cursor.prev_column(),
        }
    }
    fn handle_right(&mut self) {
        match self.mode {
            UIMode::View => self.get_top_layer_mut().cursor.next_column_i(),
            _ => self.get_top_layer_mut().cursor.next_column(),
        }
    }
    fn handle_up(&mut self) {
        match self.mode {
            UIMode::View => self.get_top_layer_mut().cursor.prev_row_i(),
            _ => self.get_top_layer_mut().cursor.prev_row(),
        }
    }
    fn handle_down(&mut self) {
        match self.mode {
            UIMode::View => self.get_top_layer_mut().cursor.next_row_i(),
            _ => self.get_top_layer_mut().cursor.next_row(),
        }
    }
}

fn create_layer(value: Value) -> Result<RecordLayer> {
    let (columns, values) = collect_input(value)?;
    if columns.is_empty() {
        return Err(anyhow::anyhow!("Nothing to explore in empty collections!"));
    }
    Ok(RecordLayer::new(columns, values))
}

fn push_layer(view: &mut RecordView, mut next_layer: RecordLayer) {
    let layer = view.get_top_layer();
    let header = layer.get_column_header();

    if let Some(header) = header {
        next_layer.set_name(header);
    }

    view.layer_stack.push(next_layer);
    view.auto_tail = false;
    view.previous_row_count = view.get_top_layer().record_values.len();
}

fn estimate_page_size(area: Rect, show_head: bool) -> u16 {
    let mut available_height = area.height;
    available_height -= 3; // status_bar

    if show_head {
        available_height -= 3; // head
    }

    available_height
}

/// scroll to the end of the data
fn tail_data(state: &mut RecordView, page_size: usize) {
    let layer = state.get_top_layer_mut();
    let count_rows = layer.record_values.len();
    if count_rows > page_size {
        layer
            .cursor
            .set_window_start_position(count_rows - page_size, 0);
    }
}

fn convert_records_to_string(
    records: &[Vec<Value>],
    cfg: &Config,
    style_computer: &StyleComputer,
) -> Vec<Vec<NuText>> {
    records
        .iter()
        .map(|row| {
            row.iter()
                .map(|value| {
                    let text = value.clone().to_abbreviated_string(cfg);
                    let text = strip_string(&text);
                    let float_precision = cfg.float_precision as usize;

                    make_styled_string(style_computer, text, Some(value), float_precision)
                })
                .collect::<Vec<_>>()
        })
        .collect::<Vec<_>>()
}

fn highlight_selected_cell(f: &mut Frame, info: ElementInfo, cfg: &ExploreConfig) {
    let cell_style = cfg.selected_cell;
    let highlight_block = Block::default().style(nu_style_to_tui(cell_style));
    let area = Rect::new(info.area.x, info.area.y, info.area.width, 1);
    f.render_widget(highlight_block.clone(), area)
}

fn report_cursor_position(mode: UIMode, cursor: WindowCursor2D) -> String {
    if mode == UIMode::Cursor {
        let Position { row, column } = cursor.position();
        format!("{row},{column}")
    } else {
        let Position { row, column } = cursor.window_origin();
        format!("{row},{column}")
    }
}

fn report_row_position(cursor: WindowCursor2D) -> String {
    if cursor.window_origin().row == 0 {
        String::from("Top")
    } else {
        let percent_rows = get_percentage(cursor.row(), cursor.row_limit());

        match percent_rows {
            100 => String::from("All"),
            value => format!("{value}%"),
        }
    }
}

fn get_percentage(value: usize, max: usize) -> usize {
    debug_assert!(value <= max, "{value:?} {max:?}");

    ((value as f32 / max as f32) * 100.0).floor() as usize
}

fn transpose_table(layer: &mut RecordLayer) {
    if layer.was_transposed {
        transpose_from(layer);
    } else {
        transpose_to(layer);
    }

    layer.was_transposed = !layer.was_transposed;
}

fn transpose_from(layer: &mut RecordLayer) {
    let count_rows = layer.record_values.len();
    let count_columns = layer.column_names.len();

    // Extract the original column names from the first column
    let headers = pop_first_column(&mut layer.record_values);
    let headers = headers
        .into_iter()
        .map(|value| match value {
            Value::String { val, .. } => val,
            _ => unreachable!("must never happen"),
        })
        .collect();

    let data = _transpose_table(&layer.record_values, count_rows, count_columns - 1);

    layer.record_values = data;
    layer.column_names = headers;
    // Invalidate the text cache so it gets regenerated with the new structure
    layer.record_text = None;
}

fn pop_first_column<T>(values: &mut [Vec<T>]) -> Vec<T>
where
    T: Default + Clone,
{
    let mut data = vec![T::default(); values.len()];
    for (row, values) in values.iter_mut().enumerate() {
        data[row] = values.remove(0);
    }

    data
}

fn transpose_to(layer: &mut RecordLayer) {
    let count_rows = layer.record_values.len();
    let count_columns = layer.column_names.len();

    let mut data = _transpose_table(&layer.record_values, count_rows, count_columns);
    for (column, column_name) in layer.column_names.iter().enumerate() {
        let value = Value::string(column_name, NuSpan::unknown());
        data[column].insert(0, value);
    }

    layer.record_values = data;
    layer.column_names = (1..=count_rows + 1).map(|i| i.to_string()).collect();
    // Invalidate the text cache so it gets regenerated with the new structure
    layer.record_text = None;
}

fn _transpose_table<T>(values: &[Vec<T>], count_rows: usize, count_columns: usize) -> Vec<Vec<T>>
where
    T: Clone + Default,
{
    let mut data = vec![vec![T::default(); count_rows]; count_columns];
    for (row, values) in values.iter().enumerate() {
        for (column, value) in values.iter().enumerate() {
            data[column][row].clone_from(value);
        }
    }

    data
}

fn strip_string(text: &str) -> String {
    String::from_utf8(strip_ansi_escapes::strip(text))
        .map_err(|_| ())
        .unwrap_or_else(|_| text.to_owned())
}

#[cfg(test)]
mod tests {
    use super::*;
    use nu_protocol::{Value, span::Span};

    // Helper to create a simple test Value::Record
    fn create_test_record() -> Value {
        let mut record = nu_protocol::Record::new();
        record.insert(
            "name".to_string(),
            Value::string("sample", Span::test_data()),
        );
        record.insert("value".to_string(), Value::int(42, Span::test_data()));
        Value::record(record, Span::test_data())
    }

    // Helper to create a simple test Value::List
    fn create_test_list() -> Value {
        let items = vec![
            Value::string("item1", Span::test_data()),
            Value::string("item2", Span::test_data()),
        ];
        Value::list(items, Span::test_data())
    }

    #[test]
    fn test_create_layer_empty_collection() {
        // Test with empty record
        let empty_record = Value::record(nu_protocol::Record::new(), Span::test_data());
        let result = create_layer(empty_record);
        assert!(result.is_err());
        assert_eq!(
            result.unwrap_err().to_string(),
            "Nothing to explore in empty collections!"
        );

        // Test with empty list
        let empty_list = Value::list(vec![], Span::test_data());
        let result = create_layer(empty_list);
        assert!(result.is_err());
        assert_eq!(
            result.unwrap_err().to_string(),
            "Nothing to explore in empty collections!"
        );
    }

    #[test]
    fn test_create_layer_valid_record() {
        let record = create_test_record();
        let result = create_layer(record);
        assert!(result.is_ok());

        let layer = result.unwrap();
        assert_eq!(layer.column_names, vec!["name", "value"]);
        assert_eq!(layer.record_values.len(), 1);
        assert_eq!(layer.record_values[0].len(), 2);
    }

    #[test]
    fn test_create_layer_valid_list() {
        let list = create_test_list();
        let result = create_layer(list);
        assert!(result.is_ok());

        let layer = result.unwrap();
        assert_eq!(layer.column_names, vec![""]);
        assert_eq!(layer.record_values.len(), 2);
        assert_eq!(layer.record_values[0].len(), 1);
    }

    #[test]
    fn test_transpose_table() {
        // Create a simple 2x3 table
        let mut layer = RecordLayer::new(
            vec!["col1".to_string(), "col2".to_string(), "col3".to_string()],
            vec![
                vec![
                    Value::string("r1c1", Span::test_data()),
                    Value::string("r1c2", Span::test_data()),
                    Value::string("r1c3", Span::test_data()),
                ],
                vec![
                    Value::string("r2c1", Span::test_data()),
                    Value::string("r2c2", Span::test_data()),
                    Value::string("r2c3", Span::test_data()),
                ],
            ],
        );

        // Before transpose
        assert_eq!(layer.count_rows(), 2);
        assert_eq!(layer.count_columns(), 3);
        assert!(!layer.was_transposed);

        // After transpose
        transpose_table(&mut layer);
        assert_eq!(layer.count_rows(), 3);
        assert_eq!(layer.count_columns(), 3); // Now includes header row
        assert!(layer.was_transposed);

        // After transpose back
        transpose_table(&mut layer);
        assert_eq!(layer.count_rows(), 2);
        assert_eq!(layer.count_columns(), 3);
        assert!(!layer.was_transposed);
    }

    #[test]
    fn test_estimate_page_size() {
        // Test with header
        let area = Rect::new(0, 0, 80, 24);
        assert_eq!(estimate_page_size(area, true), 18); // 24 - 3 (status bar) - 3 (header) = 18

        // Test without header
        assert_eq!(estimate_page_size(area, false), 21); // 24 - 3 (status bar) = 21
    }
}
