mod highlight;
mod line;
mod links;
mod styles;
mod table;
mod wrap;

use std::path::{Path, PathBuf};

use pulldown_cmark::{
    Alignment, CodeBlockKind, CowStr, Event, HeadingLevel, Options, Parser, Tag, TagEnd,
};
use ratatui::style::Style;
use ratatui::text::{Line, Span, Text};

pub use highlight::set_syntax_theme_by_name;
pub use highlight::spawn_background_load;
pub(crate) use highlight::{highlight_code_to_lines, highlight_code_to_lines_for_path};
pub use line::is_blank_line_spaces_only;
use line::push_owned_lines;
use wrap::{RtOptions, adaptive_wrap_line};
pub use wrap::{RtOptions as WrapOptions, adaptive_wrap_lines, word_wrap_line};

pub use links::is_local_path_like_link;

use links::{render_local_link_target, should_render_link_destination};
use styles::MarkdownStyles;
use table::TableState;

pub fn render_markdown_text(input: &str) -> Text<'static> {
    render_markdown_text_with_width(input, None)
}

pub(crate) fn render_markdown_text_with_width(input: &str, width: Option<usize>) -> Text<'static> {
    let cwd = std::env::current_dir().ok();
    render_markdown_text_with_width_and_cwd(input, width, cwd.as_deref())
}

pub(crate) fn render_markdown_text_with_width_and_cwd(
    input: &str,
    width: Option<usize>,
    cwd: Option<&Path>,
) -> Text<'static> {
    let mut options = Options::empty();
    options.insert(Options::ENABLE_STRIKETHROUGH);
    options.insert(Options::ENABLE_TABLES);
    let parser = Parser::new_ext(input, options);
    let mut writer = Writer::new(parser, cwd);
    writer.wrap_width = width;
    writer.run();
    writer.text
}

#[derive(Clone, Debug)]
struct IndentContext {
    prefix: Vec<Span<'static>>,
    marker: Option<Vec<Span<'static>>>,
    is_list: bool,
}

impl IndentContext {
    fn new(prefix: Vec<Span<'static>>, marker: Option<Vec<Span<'static>>>, is_list: bool) -> Self {
        Self {
            prefix,
            marker,
            is_list,
        }
    }
}

#[derive(Clone, Debug)]
struct LinkState {
    destination: String,
    show_destination: bool,
    local_target_display: Option<String>,
}

struct Writer<'a, I>
where
    I: Iterator<Item = Event<'a>>,
{
    iter: I,
    text: Text<'static>,
    styles: MarkdownStyles,
    inline_styles: Vec<Style>,
    indent_stack: Vec<IndentContext>,
    list_indices: Vec<Option<u64>>,
    link: Option<LinkState>,
    needs_newline: bool,
    pending_marker_line: bool,
    in_paragraph: bool,
    in_code_block: bool,
    code_block_lang: Option<String>,
    code_block_buffer: String,
    cwd: Option<PathBuf>,
    line_ends_with_local_link_target: bool,
    pending_local_link_soft_break: bool,
    current_line_content: Option<Line<'static>>,
    current_initial_indent: Vec<Span<'static>>,
    current_subsequent_indent: Vec<Span<'static>>,
    current_line_style: Style,
    current_line_in_code_block: bool,
    wrap_width: Option<usize>,
    table_state: Option<TableState>,
    in_table_cell: bool,
}

impl<'a, I> Writer<'a, I>
where
    I: Iterator<Item = Event<'a>>,
{
    fn new(iter: I, cwd: Option<&Path>) -> Self {
        Self {
            iter,
            text: Text::default(),
            styles: MarkdownStyles::default(),
            inline_styles: Vec::new(),
            indent_stack: Vec::new(),
            list_indices: Vec::new(),
            link: None,
            needs_newline: false,
            pending_marker_line: false,
            in_paragraph: false,
            in_code_block: false,
            code_block_lang: None,
            code_block_buffer: String::new(),
            cwd: cwd.map(Path::to_path_buf),
            line_ends_with_local_link_target: false,
            pending_local_link_soft_break: false,
            current_line_content: None,
            current_initial_indent: Vec::new(),
            current_subsequent_indent: Vec::new(),
            current_line_style: Style::default(),
            current_line_in_code_block: false,
            wrap_width: None,
            table_state: None,
            in_table_cell: false,
        }
    }

    fn run(&mut self) {
        while let Some(event) = self.iter.next() {
            self.handle_event(event);
        }
        self.flush_current_line();
    }

    fn handle_event(&mut self, event: Event<'a>) {
        self.prepare_for_event(&event);
        match event {
            Event::Start(tag) => self.start_tag(tag),
            Event::End(tag) => self.end_tag(tag),
            Event::Text(text) => self.text(text),
            Event::Code(code) => self.code(code),
            Event::InlineMath(math) => self.text(math),
            Event::DisplayMath(math) => self.text(math),
            Event::SoftBreak => self.soft_break(),
            Event::HardBreak => self.hard_break(),
            Event::Rule => {
                self.flush_current_line();
                if !self.text.lines.is_empty() {
                    self.push_blank_line();
                }
                self.push_line(Line::from("———"));
                self.needs_newline = true;
            }
            Event::Html(html) => self.html(html, false),
            Event::InlineHtml(html) => self.html(html, true),
            Event::FootnoteReference(_) => {}
            Event::TaskListMarker(_) => {}
        }
    }

    fn prepare_for_event(&mut self, event: &Event<'a>) {
        if !self.pending_local_link_soft_break {
            return;
        }

        if matches!(event, Event::Text(text) if text.trim_start().starts_with(':')) {
            self.pending_local_link_soft_break = false;
            return;
        }

        self.pending_local_link_soft_break = false;
        self.push_line(Line::default());
    }

    fn start_tag(&mut self, tag: Tag<'a>) {
        match tag {
            Tag::Paragraph => self.start_paragraph(),
            Tag::Heading { level, .. } => self.start_heading(level),
            Tag::BlockQuote(_) => self.start_blockquote(),
            Tag::CodeBlock(kind) => {
                let indent = match kind {
                    CodeBlockKind::Fenced(_) => None,
                    CodeBlockKind::Indented => Some(Span::from(" ".repeat(4))),
                };
                let lang = match kind {
                    CodeBlockKind::Fenced(lang) => Some(lang.to_string()),
                    CodeBlockKind::Indented => None,
                };
                self.start_codeblock(lang, indent)
            }
            Tag::List(start) => self.start_list(start),
            Tag::Item => self.start_item(),
            Tag::Table(alignments) => self.start_table(alignments),
            Tag::TableHead => self.start_table_head(),
            Tag::TableRow => self.start_table_row(),
            Tag::TableCell => self.start_table_cell(),
            Tag::Emphasis => self.push_inline_style(self.styles.emphasis),
            Tag::Strong => self.push_inline_style(self.styles.strong),
            Tag::Strikethrough => self.push_inline_style(self.styles.strikethrough),
            Tag::Link { dest_url, .. } => self.push_link(dest_url.to_string()),
            Tag::HtmlBlock
            | Tag::FootnoteDefinition(_)
            | Tag::Image { .. }
            | Tag::MetadataBlock(_) => {}
            _ => {}
        }
    }

    fn end_tag(&mut self, tag: TagEnd) {
        match tag {
            TagEnd::Paragraph => self.end_paragraph(),
            TagEnd::Heading(_) => self.end_heading(),
            TagEnd::BlockQuote(_) => self.end_blockquote(),
            TagEnd::CodeBlock => self.end_codeblock(),
            TagEnd::List(_) => self.end_list(),
            TagEnd::Item => {
                self.indent_stack.pop();
                self.pending_marker_line = false;
            }
            TagEnd::Table => self.end_table(),
            TagEnd::TableHead => self.end_table_head(),
            TagEnd::TableRow => self.end_table_row(),
            TagEnd::TableCell => self.end_table_cell(),
            TagEnd::Emphasis | TagEnd::Strong | TagEnd::Strikethrough => self.pop_inline_style(),
            TagEnd::Link => self.pop_link(),
            TagEnd::HtmlBlock
            | TagEnd::FootnoteDefinition
            | TagEnd::Image
            | TagEnd::MetadataBlock(_) => {}
            _ => {}
        }
    }

    fn start_paragraph(&mut self) {
        if self.needs_newline {
            self.push_blank_line();
        }
        self.push_line(Line::default());
        self.needs_newline = false;
        self.in_paragraph = true;
    }

    fn end_paragraph(&mut self) {
        self.needs_newline = true;
        self.in_paragraph = false;
        self.pending_marker_line = false;
    }

    fn start_heading(&mut self, level: HeadingLevel) {
        if self.needs_newline {
            self.push_line(Line::default());
            self.needs_newline = false;
        }
        let heading_style = match level {
            HeadingLevel::H1 => self.styles.h1,
            HeadingLevel::H2 => self.styles.h2,
            HeadingLevel::H3 => self.styles.h3,
            HeadingLevel::H4 => self.styles.h4,
            HeadingLevel::H5 => self.styles.h5,
            HeadingLevel::H6 => self.styles.h6,
        };
        self.push_line(Line::default());
        self.push_inline_style(heading_style);
        self.needs_newline = false;
    }

    fn end_heading(&mut self) {
        self.needs_newline = true;
        self.pop_inline_style();
    }

    fn start_blockquote(&mut self) {
        if self.needs_newline {
            self.push_blank_line();
            self.needs_newline = false;
        }
        self.indent_stack
            .push(IndentContext::new(vec![Span::from("> ")], None, false));
    }

    fn end_blockquote(&mut self) {
        self.indent_stack.pop();
        self.needs_newline = true;
    }

    fn start_table(&mut self, alignments: Vec<Alignment>) {
        self.flush_current_line();
        if !self.text.lines.is_empty() {
            self.push_blank_line();
        }

        self.pending_marker_line = false;
        self.in_table_cell = false;
        self.table_state = Some(TableState::new(
            self.prefix_spans(false),
            self.current_line_style,
            alignments,
        ));
        self.needs_newline = false;
    }

    fn end_table(&mut self) {
        self.flush_current_line();
        if let Some(table) = self.table_state.take() {
            self.text.lines.extend(table.render(self.wrap_width));
        }
        self.in_table_cell = false;
        self.needs_newline = true;
    }

    fn start_table_head(&mut self) {
        if let Some(table) = self.table_state.as_mut() {
            table.start_head();
            table.start_row();
        }
    }

    fn end_table_head(&mut self) {
        if let Some(table) = self.table_state.as_mut() {
            table.finish_row();
            table.end_head();
        }
    }

    fn start_table_row(&mut self) {
        self.flush_current_line();
        if let Some(table) = self.table_state.as_mut()
            && !(table.in_head && table.current_row.is_some())
        {
            table.start_row();
        }
        self.in_table_cell = false;
    }

    fn end_table_row(&mut self) {
        self.flush_current_line();
        if let Some(table) = self.table_state.as_mut() {
            table.finish_row();
        }
        self.in_table_cell = false;
    }

    fn start_table_cell(&mut self) {
        self.flush_current_line();
        self.in_table_cell = true;
        self.current_line_content = Some(Line::default());
        self.current_initial_indent.clear();
        self.current_subsequent_indent.clear();
        self.current_line_style = self
            .table_state
            .as_ref()
            .map(|table| table.base_style)
            .unwrap_or_default();
        self.current_line_in_code_block = false;
    }

    fn end_table_cell(&mut self) {
        self.flush_current_line();
        self.in_table_cell = false;
    }

    fn table_text(&mut self, text: CowStr<'a>) {
        let style = self.inline_styles.last().copied().unwrap_or_default();
        let mut parts = text.split('\n').peekable();
        while let Some(part) = parts.next() {
            if !part.is_empty() {
                self.push_span(Span::styled(part.to_string(), style));
            }
            if parts.peek().is_some() {
                self.push_span(Span::from(" "));
            }
        }
    }

    fn text(&mut self, text: CowStr<'a>) {
        if self.suppressing_local_link_label() {
            return;
        }
        self.line_ends_with_local_link_target = false;
        if self.in_table_cell {
            self.table_text(text);
            return;
        }
        if self.pending_marker_line {
            self.push_line(Line::default());
        }
        self.pending_marker_line = false;

        if self.in_code_block {
            self.code_block_buffer.push_str(&text);
            return;
        }

        for (index, line) in text.lines().enumerate() {
            if self.needs_newline {
                self.push_line(Line::default());
                self.needs_newline = false;
            }
            if index > 0 {
                self.push_line(Line::default());
            }
            let span = Span::styled(
                line.to_string(),
                self.inline_styles.last().copied().unwrap_or_default(),
            );
            self.push_span(span);
        }
        self.needs_newline = false;
    }

    fn code(&mut self, code: CowStr<'a>) {
        if self.suppressing_local_link_label() {
            return;
        }
        self.line_ends_with_local_link_target = false;
        if self.in_table_cell {
            self.push_span(Span::from(code.into_string()).style(self.styles.code));
            return;
        }
        if self.pending_marker_line {
            self.push_line(Line::default());
            self.pending_marker_line = false;
        }
        let span = Span::from(code.into_string()).style(self.styles.code);
        self.push_span(span);
    }

    fn html(&mut self, html: CowStr<'a>, inline: bool) {
        if self.suppressing_local_link_label() {
            return;
        }
        self.line_ends_with_local_link_target = false;
        if self.in_table_cell {
            self.table_text(html);
            return;
        }
        self.pending_marker_line = false;
        for (index, line) in html.lines().enumerate() {
            if self.needs_newline {
                self.push_line(Line::default());
                self.needs_newline = false;
            }
            if index > 0 {
                self.push_line(Line::default());
            }
            let style = self.inline_styles.last().copied().unwrap_or_default();
            self.push_span(Span::styled(line.to_string(), style));
        }
        self.needs_newline = !inline;
    }

    fn hard_break(&mut self) {
        if self.suppressing_local_link_label() {
            return;
        }
        if self.in_table_cell {
            self.push_span(Span::from(" "));
            return;
        }
        if self.in_code_block {
            self.code_block_buffer.push('\n');
            return;
        }
        self.line_ends_with_local_link_target = false;
        self.push_line(Line::default());
    }

    fn soft_break(&mut self) {
        if self.suppressing_local_link_label() {
            return;
        }
        if self.in_table_cell {
            self.push_span(Span::from(" "));
            return;
        }
        if self.in_code_block {
            self.code_block_buffer.push('\n');
            return;
        }
        if self.line_ends_with_local_link_target {
            self.pending_local_link_soft_break = true;
            self.line_ends_with_local_link_target = false;
            return;
        }
        self.line_ends_with_local_link_target = false;
        self.push_line(Line::default());
    }

    fn start_list(&mut self, index: Option<u64>) {
        if self.list_indices.is_empty() && self.needs_newline {
            self.push_line(Line::default());
        }
        self.list_indices.push(index);
    }

    fn end_list(&mut self) {
        self.list_indices.pop();
        self.needs_newline = true;
    }

    fn start_item(&mut self) {
        self.pending_marker_line = true;
        let depth = self.list_indices.len();
        let is_ordered = self
            .list_indices
            .last()
            .map(Option::is_some)
            .unwrap_or(false);
        let width = depth * 4 - 3;
        let marker = if let Some(last_index) = self.list_indices.last_mut() {
            match last_index {
                None => Some(vec![Span::styled(
                    " ".repeat(width - 1) + "- ",
                    self.styles.unordered_list_marker,
                )]),
                Some(index) => {
                    *index += 1;
                    Some(vec![Span::styled(
                        format!("{:width$}. ", *index - 1),
                        self.styles.ordered_list_marker,
                    )])
                }
            }
        } else {
            None
        };
        let indent_prefix = if depth == 0 {
            Vec::new()
        } else {
            let indent_len = if is_ordered { width + 2 } else { width + 1 };
            vec![Span::from(" ".repeat(indent_len))]
        };
        self.indent_stack
            .push(IndentContext::new(indent_prefix, marker, true));
        self.needs_newline = false;
    }

    fn start_codeblock(&mut self, lang: Option<String>, indent: Option<Span<'static>>) {
        self.flush_current_line();
        if !self.text.lines.is_empty() {
            self.push_blank_line();
        }
        self.in_code_block = true;
        self.code_block_lang = lang
            .as_deref()
            .and_then(|value| value.split([',', ' ', '\t']).next())
            .filter(|value| !value.is_empty())
            .map(std::string::ToString::to_string);
        self.code_block_buffer.clear();
        self.indent_stack.push(IndentContext::new(
            vec![indent.unwrap_or_default()],
            None,
            false,
        ));
        self.needs_newline = true;
    }

    fn end_codeblock(&mut self) {
        let code = std::mem::take(&mut self.code_block_buffer);
        if let Some(lang) = self.code_block_lang.take() {
            if !code.is_empty() {
                let highlighted = highlight_code_to_lines(&code, &lang);
                for line in highlighted {
                    self.push_line(Line::default());
                    for span in line.spans {
                        self.push_span(span);
                    }
                }
            } else {
                self.push_line(Line::default());
            }
        } else if !code.is_empty() {
            for line in code.lines() {
                self.push_line(Line::default());
                self.push_span(Span::styled(line.to_string(), self.styles.code));
            }
        } else {
            self.push_line(Line::default());
        }

        self.needs_newline = true;
        self.in_code_block = false;
        self.indent_stack.pop();
    }

    fn push_inline_style(&mut self, style: Style) {
        let current = self.inline_styles.last().copied().unwrap_or_default();
        self.inline_styles.push(current.patch(style));
    }

    fn pop_inline_style(&mut self) {
        self.inline_styles.pop();
    }

    fn push_link(&mut self, dest_url: String) {
        let show_destination = should_render_link_destination(&dest_url);
        self.link = Some(LinkState {
            show_destination,
            local_target_display: if is_local_path_like_link(&dest_url) {
                render_local_link_target(&dest_url, self.cwd.as_deref())
            } else {
                None
            },
            destination: dest_url,
        });
    }

    fn pop_link(&mut self) {
        if let Some(link) = self.link.take() {
            if link.show_destination {
                self.push_span(" (".into());
                self.push_span(Span::styled(link.destination, self.styles.link));
                self.push_span(")".into());
            } else if let Some(local_target_display) = link.local_target_display {
                if self.pending_marker_line {
                    self.push_line(Line::default());
                }
                let style = self
                    .inline_styles
                    .last()
                    .copied()
                    .unwrap_or_default()
                    .patch(self.styles.code);
                self.push_span(Span::styled(local_target_display, style));
                self.line_ends_with_local_link_target = true;
            }
        }
    }

    fn suppressing_local_link_label(&self) -> bool {
        self.link
            .as_ref()
            .and_then(|link| link.local_target_display.as_ref())
            .is_some()
    }

    fn flush_current_line(&mut self) {
        if let Some(line) = self.current_line_content.take() {
            if self.in_table_cell {
                if let Some(table) = self.table_state.as_mut() {
                    table.push_cell(line.style(self.current_line_style));
                }
                self.in_table_cell = false;
                self.current_initial_indent.clear();
                self.current_subsequent_indent.clear();
                self.current_line_in_code_block = false;
                self.line_ends_with_local_link_target = false;
                return;
            }

            let style = self.current_line_style;
            let line = line.style(style);

            let should_wrap =
                self.wrap_width.is_some_and(|width| width > 0) && !line.spans.is_empty();

            if should_wrap {
                let width = self.wrap_width.expect("wrap_width checked above");
                if self.current_line_in_code_block {
                    // Code blocks should be wrapped as a whole line to preserve multiple spans (colors)
                    let wrapped = word_wrap_line(
                        &line,
                        RtOptions::new(width)
                            .initial_indent(Line::from(self.current_initial_indent.clone()))
                            .subsequent_indent(Line::from(self.current_subsequent_indent.clone()))
                            .break_words(true),
                    );

                    for w_line in wrapped {
                        // Convert back to owned lines to satisfy 'static requirement
                        let owned_spans: Vec<Span<'static>> = w_line
                            .spans
                            .into_iter()
                            .map(|s| Span::styled(s.content.to_string(), s.style))
                            .collect();
                        self.text.lines.push(Line::from(owned_spans).style(style));
                    }
                } else {
                    let wrapped = adaptive_wrap_line(
                        &line,
                        RtOptions::new(width)
                            .initial_indent(Line::from(self.current_initial_indent.clone()))
                            .subsequent_indent(Line::from(self.current_subsequent_indent.clone())),
                    );
                    push_owned_lines(&wrapped, &mut self.text.lines);
                }
            } else {
                let mut spans = self.current_initial_indent.clone();
                let mut line = line;
                spans.append(&mut line.spans);
                self.text.lines.push(Line::from_iter(spans).style(style));
            }
            self.current_initial_indent.clear();
            self.current_subsequent_indent.clear();
            self.current_line_in_code_block = false;
            self.line_ends_with_local_link_target = false;
        }
    }

    fn push_line(&mut self, line: Line<'static>) {
        self.flush_current_line();
        let blockquote_active = self
            .indent_stack
            .iter()
            .any(|context| context.prefix.iter().any(|span| span.content.contains('>')));
        let style = if blockquote_active {
            self.styles.blockquote
        } else {
            line.style
        };
        let was_pending = self.pending_marker_line;

        self.current_initial_indent = self.prefix_spans(was_pending);
        self.current_subsequent_indent = self.prefix_spans(false);
        self.current_line_style = style;
        self.current_line_content = Some(line);
        self.current_line_in_code_block = self.in_code_block;
        self.line_ends_with_local_link_target = false;

        self.pending_marker_line = false;
    }

    fn push_span(&mut self, span: Span<'static>) {
        if self.in_table_cell && self.current_line_content.is_none() {
            self.current_line_content = Some(Line::default());
        }
        if let Some(line) = self.current_line_content.as_mut() {
            line.push_span(span);
        } else {
            self.push_line(Line::from(vec![span]));
        }
    }

    fn push_blank_line(&mut self) {
        self.flush_current_line();
        if self.indent_stack.iter().all(|context| context.is_list) {
            self.text.lines.push(Line::default());
        } else {
            self.push_line(Line::default());
            self.flush_current_line();
        }
    }

    fn prefix_spans(&self, pending_marker_line: bool) -> Vec<Span<'static>> {
        let mut prefix = Vec::new();
        let last_marker_index = if pending_marker_line {
            self.indent_stack
                .iter()
                .enumerate()
                .rev()
                .find_map(|(index, context)| context.marker.as_ref().map(|_| index))
        } else {
            None
        };
        let last_list_index = self
            .indent_stack
            .iter()
            .rposition(|context| context.is_list);

        for (index, context) in self.indent_stack.iter().enumerate() {
            if pending_marker_line {
                if Some(index) == last_marker_index
                    && let Some(marker) = &context.marker
                {
                    prefix.extend(marker.iter().cloned());
                    continue;
                }
                if context.is_list
                    && last_marker_index.is_some_and(|marker_index| marker_index > index)
                {
                    continue;
                }
            } else if context.is_list && Some(index) != last_list_index {
                continue;
            }
            prefix.extend(context.prefix.iter().cloned());
        }

        prefix
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn lines_to_strings(text: &Text<'_>) -> Vec<String> {
        text.lines
            .iter()
            .map(|line| {
                line.spans
                    .iter()
                    .map(|span| span.content.clone())
                    .collect::<String>()
            })
            .collect()
    }

    #[test]
    fn renders_simple_heading() {
        let text = render_markdown_text("# Title\n");
        let rendered = lines_to_strings(&text);
        assert_eq!(rendered, vec!["Title".to_string()]);
    }

    #[test]
    fn renders_list_item() {
        let text = render_markdown_text("- item\n");
        let rendered = lines_to_strings(&text);
        assert_eq!(rendered, vec!["- item".to_string()]);
    }

    #[test]
    fn renders_code_block() {
        let text = render_markdown_text("```rust\nfn main() {}\n```\n");
        let rendered = lines_to_strings(&text);
        assert_eq!(rendered, vec!["fn main() {}".to_string()]);
    }

    #[test]
    fn renders_local_file_link() {
        let text = render_markdown_text_with_width_and_cwd(
            "See [file](/workspace/project/src/lib.rs:12) for details.",
            None,
            Some(Path::new("/workspace/project")),
        );
        let rendered = lines_to_strings(&text);
        assert_eq!(rendered, vec!["See src/lib.rs:12 for details.".to_string()]);
    }

    #[test]
    fn renders_markdown_table() {
        let text = render_markdown_text(
            "| Name | Count |\n|:-----|------:|\n| a | 1 |\n| longer | 23 |\n",
        );
        let rendered = lines_to_strings(&text);
        assert_eq!(
            rendered,
            vec![
                "┌────────┬───────┐".to_string(),
                "│ Name   │ Count │".to_string(),
                "├────────┼───────┤".to_string(),
                "│ a      │     1 │".to_string(),
                "├────────┼───────┤".to_string(),
                "│ longer │    23 │".to_string(),
                "└────────┴───────┘".to_string(),
            ]
        );
    }

    #[test]
    fn renders_markdown_table_in_narrow_space() {
        let text = render_markdown_text_with_width(
            "| Name | Count |\n|:-----|------:|\n| a long value | 12345 |\n| another row | 7 |\n",
            Some(12),
        );
        let rendered = lines_to_strings(&text);

        assert!(rendered.first().is_some_and(|line| line.starts_with('┌')));
        assert!(rendered.iter().any(|line| line.contains("Name:")));
        assert!(rendered.iter().any(|line| line.contains("Count:")));
        assert!(rendered.iter().any(|line| line.contains("long")));
        assert!(rendered.last().is_some_and(|line| line.starts_with('└')));
    }
}
