Skip to main content

scope/display/
terminal.rs

1//! # Terminal Display Helpers
2//!
3//! Styled output utilities for rich, user-friendly terminal presentation.
4//! Text is painted by semantic role through [`super::theme`], with
5//! `unicode` box-drawing characters for structure.
6//! All helpers respect non-TTY contexts (piped output) by falling back to plain text.
7//! Long text is automatically word-wrapped to the detected terminal width.
8
9use super::theme::{Painter, Role};
10use crate::market::health::{PegBand, PegBands};
11use std::sync::atomic::{AtomicUsize, Ordering};
12use unicode_width::UnicodeWidthStr;
13
14// ============================================================================
15// Terminal detection
16// ============================================================================
17
18/// The color decision lives in the theme; these paths stay for callers.
19pub use super::theme::{
20    color_enabled, color_enabled_stderr, color_enabled_with, set_color_override,
21};
22
23/// Width override in columns: 0 = none. The TUI sets it to the width of
24/// its output pane, so command output fits the pane.
25static WIDTH_OVERRIDE: AtomicUsize = AtomicUsize::new(0);
26
27/// Sets the width that [`terminal_width`] returns for the whole process,
28/// or removes the override (`None`). `Some(0)` also removes it. The TUI
29/// removes it each time it gives the terminal back.
30pub fn set_width_override(width: Option<usize>) {
31    WIDTH_OVERRIDE.store(width.unwrap_or(0), Ordering::Relaxed);
32}
33
34/// The width override, if one is set.
35pub fn width_override() -> Option<usize> {
36    match WIDTH_OVERRIDE.load(Ordering::Relaxed) {
37        0 => None,
38        w => Some(w),
39    }
40}
41
42/// Returns the current terminal width in columns.
43///
44/// Uses the width override when one is set (the TUI pane). Otherwise uses
45/// `crossterm::terminal::size()` when available, and falls back to 80
46/// columns.
47pub fn terminal_width() -> usize {
48    width_or_real(width_override())
49}
50
51/// `over`, or the real terminal width when it is `None`.
52fn width_or_real(over: Option<usize>) -> usize {
53    over.unwrap_or_else(|| {
54        crossterm::terminal::size()
55            .map(|(w, _)| w as usize)
56            .unwrap_or(80)
57    })
58}
59
60/// Serialises the tests that set the width override or depend on
61/// `terminal_width()`.
62#[cfg(test)]
63pub(crate) fn width_lock() -> std::sync::MutexGuard<'static, ()> {
64    static WIDTH_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
65    WIDTH_LOCK.lock().unwrap_or_else(|p| p.into_inner())
66}
67
68/// The widest rule, in columns, that the section helpers draw.
69const RULE_WIDTH: usize = 50;
70
71/// The `─` run of a rule of `len` columns that starts after `prefix_cols`
72/// columns. Only the width override (`cap`, the TUI pane) shortens it:
73/// with no override a rule keeps its fixed width, so CLI and piped output
74/// stay the same at every terminal width.
75fn rule_at(len: usize, prefix_cols: usize, cap: Option<usize>) -> String {
76    let len = match cap {
77        Some(w) => len.min(w.saturating_sub(prefix_cols)),
78        None => len,
79    };
80    "─".repeat(len)
81}
82
83// ============================================================================
84// Word-wrapping engine
85// ============================================================================
86
87/// Word-wrap `text` so each line fits within `content_width` columns.
88///
89/// Returns a `Vec<String>` where the first element is the first line of text
90/// and subsequent elements are continuation lines. Words are split on whitespace;
91/// a word longer than the content width is kept on its own line unbroken (e.g. URLs).
92///
93/// This function operates on the raw text — callers apply the box-drawing prefix
94/// and styling to each returned line.
95pub fn wrap_lines(text: &str, content_width: usize) -> Vec<String> {
96    if content_width == 0 {
97        return vec![text.to_string()];
98    }
99
100    let mut lines: Vec<String> = Vec::new();
101    let mut current_line = String::new();
102    let mut current_len = 0usize;
103
104    for word in text.split_whitespace() {
105        let word_len = word.width();
106
107        if current_len == 0 {
108            // First word on the line — always accept it even if it exceeds width
109            current_line.push_str(word);
110            current_len = word_len;
111        } else if current_len + 1 + word_len <= content_width {
112            // Fits on current line with a space
113            current_line.push(' ');
114            current_line.push_str(word);
115            current_len += 1 + word_len;
116        } else {
117            // Doesn't fit — wrap to next line
118            lines.push(current_line);
119            current_line = word.to_string();
120            current_len = word_len;
121        }
122    }
123
124    if !current_line.is_empty() {
125        lines.push(current_line);
126    }
127
128    if lines.is_empty() {
129        lines.push(String::new());
130    }
131
132    lines
133}
134
135/// Compute the available content width for a given prefix.
136///
137/// `prefix_width` is the number of visible columns consumed by the prefix
138/// (e.g. "│  " = 3, "│      " = 7, "│    • " = 6).
139/// Returns the remaining columns for text content.
140fn content_width_for(prefix_width: usize) -> usize {
141    terminal_width().saturating_sub(prefix_width)
142}
143
144// ============================================================================
145// Color-aware helpers
146// ============================================================================
147
148/// The visible columns of a marked row's prefix: `"│  ✓ "`.
149const MARK_PREFIX_COLS: usize = 5;
150
151/// The visible width of a key or label column.
152const LABEL_COLS: usize = 18;
153
154/// The vertical rule that opens every row inside a section.
155fn rail(p: Painter) -> String {
156    p.paint(Role::Accent, "│")
157}
158
159/// A key or label padded to [`LABEL_COLS`], in the secondary tone. The
160/// padding sits inside the paint, so colored and plain rows align alike.
161fn label(p: Painter, text: &str) -> String {
162    p.paint(Role::Secondary, &format!("{:<LABEL_COLS$}", text))
163}
164
165/// A row that starts with a glyph (`✓`, `✗`, `!`, `–`, `⚠`, `ℹ`), its
166/// text wrapped under the glyph in `text_role`.
167fn marked_row(p: Painter, glyph: String, text_role: Role, msg: &str) -> String {
168    let wrapped = wrap_lines(msg, content_width_for(MARK_PREFIX_COLS));
169    let mut out = format!("{}  {} {}", rail(p), glyph, p.paint(text_role, &wrapped[0]));
170    for line in &wrapped[1..] {
171        out.push_str(&format!("\n{}    {}", rail(p), p.paint(text_role, line)));
172    }
173    out
174}
175
176/// Section header with colored title and box-drawing underline.
177///
178/// ```text
179/// ┌─ Token Health ─────────────────────────────
180/// ```
181pub fn section_header(title: &str) -> String {
182    section_header_styled(title, color_enabled())
183}
184
185fn section_header_styled(title: &str, tty: bool) -> String {
186    section_header_at(title, tty, width_override())
187}
188
189/// `section_header_styled` with the width cap given.
190fn section_header_at(title: &str, tty: bool, cap: Option<usize>) -> String {
191    let p = Painter::new(tty);
192    // "┌─ " + title + " " come before the rule.
193    let prefix = title.width() + 4;
194    let pad = rule_at(RULE_WIDTH.saturating_sub(prefix), prefix, cap);
195    format!(
196        "\n{} {} {}",
197        p.paint(Role::Accent, "┌─"),
198        p.paint_strong(Role::Accent, title),
199        p.paint(Role::Accent, &pad)
200    )
201}
202
203/// Sub-section header (lighter weight).
204///
205/// ```text
206/// │
207/// ├── DEX Analytics
208/// ```
209pub fn subsection_header(title: &str) -> String {
210    subsection_header_styled(title, color_enabled())
211}
212
213fn subsection_header_styled(title: &str, tty: bool) -> String {
214    let p = Painter::new(tty);
215    format!(
216        "{}\n{}{}",
217        rail(p),
218        p.paint(Role::Accent, "├── "),
219        p.paint_strong(Role::Accent, title)
220    )
221}
222
223/// A key-value row inside a section, with aligned values.
224///
225/// ```text
226/// │  Price            $0.9999
227/// ```
228pub fn kv_row(key: &str, value: &str) -> String {
229    kv_row_styled(key, value, color_enabled())
230}
231
232fn kv_row_styled(key: &str, value: &str, tty: bool) -> String {
233    let p = Painter::new(tty);
234    // Prefix: "│  " (3) + key pad (18) = 21 visible columns before value starts
235    let prefix_cols = 3 + LABEL_COLS;
236    let avail = terminal_width().saturating_sub(prefix_cols);
237    let wrapped = wrap_lines(value, avail);
238
239    let mut out = format!("{}  {}{}", rail(p), label(p, key), wrapped[0]);
240    // Continuation lines align under the value column
241    for line in &wrapped[1..] {
242        out.push_str(&format!("\n{}  {:<LABEL_COLS$}{}", rail(p), "", line));
243    }
244    out
245}
246
247/// A key-value row where the value is colored based on positive/negative.
248pub fn kv_row_delta(key: &str, value: f64, formatted: &str) -> String {
249    kv_row_delta_styled(key, value, formatted, color_enabled())
250}
251
252fn kv_row_delta_styled(key: &str, value: f64, formatted: &str, tty: bool) -> String {
253    let p = Painter::new(tty);
254    let role = if value > 0.0 {
255        Role::Ok
256    } else if value < 0.0 {
257        Role::Err
258    } else {
259        Role::Secondary
260    };
261    format!("{}  {}{}", rail(p), label(p, key), p.paint(role, formatted))
262}
263
264/// Health check pass line.
265///
266/// ```text
267/// │  ✓ No sells below peg
268/// ```
269pub fn check_pass(msg: &str) -> String {
270    check_pass_styled(msg, color_enabled())
271}
272
273fn check_pass_styled(msg: &str, tty: bool) -> String {
274    let p = Painter::new(tty);
275    marked_row(p, p.paint(Role::Ok, "✓"), Role::Neutral, msg)
276}
277
278/// Health check fail line.
279///
280/// ```text
281/// │  ✗ Bid depth: 0 USDT < 3000 USDT minimum
282/// ```
283pub fn check_fail(msg: &str) -> String {
284    check_fail_styled(msg, color_enabled())
285}
286
287fn check_fail_styled(msg: &str, tty: bool) -> String {
288    let p = Painter::new(tty);
289    marked_row(p, p.paint(Role::Err, "✗"), Role::Neutral, msg)
290}
291
292/// Formats a check that does not apply (dim dash, message in grey).
293///
294/// ```text
295/// │  – Spread: n/a (synthetic AMM book: the real spread is the pool fee)
296/// ```
297pub fn check_na(msg: &str) -> String {
298    check_na_styled(msg, color_enabled())
299}
300
301fn check_na_styled(msg: &str, tty: bool) -> String {
302    let p = Painter::new(tty);
303    marked_row(p, p.paint(Role::Secondary, "–"), Role::Secondary, msg)
304}
305
306/// A check row for a warning: outside its target, but not a failure.
307///
308/// ```text
309/// │  ! Depth/volume: 27.8% (13344 / 47929 USDT within ±2%; target 5–10%)
310/// ```
311pub fn check_warn(msg: &str) -> String {
312    check_warn_styled(msg, color_enabled())
313}
314
315fn check_warn_styled(msg: &str, tty: bool) -> String {
316    let p = Painter::new(tty);
317    marked_row(p, p.paint_strong(Role::Warn, "!"), Role::Warn, msg)
318}
319
320/// A warning with a name in a column of `label_cols` and its detail after
321/// it, wrapped under the detail: one signal of a list.
322///
323/// ```text
324/// │  ! Bid ladder        9 levels falling 560.112 → 110.1101 at 0.01% steps
325/// │  ! Jittered timer    90% of 499 gaps between trades are 32–58s
326/// ```
327pub fn labelled_warn(label: &str, label_cols: usize, detail: &str) -> String {
328    labelled_warn_styled(label, label_cols, detail, color_enabled())
329}
330
331fn labelled_warn_styled(label: &str, label_cols: usize, detail: &str, tty: bool) -> String {
332    let p = Painter::new(tty);
333    let indent = MARK_PREFIX_COLS + label_cols;
334    let wrapped = wrap_lines(detail, content_width_for(indent));
335    let mut out = format!(
336        "{}  {} {}{}",
337        rail(p),
338        p.paint_strong(Role::Warn, "!"),
339        p.paint(Role::Warn, &format!("{label:<label_cols$}")),
340        wrapped[0]
341    );
342    for line in &wrapped[1..] {
343        out.push_str(&format!(
344            "\n{}  {:indent$}{line}",
345            rail(p),
346            "",
347            indent = indent - 2
348        ));
349    }
350    out
351}
352
353/// Overall status line (healthy / unhealthy).
354pub fn status_line(healthy: bool) -> String {
355    status_line_styled(healthy, color_enabled())
356}
357
358fn status_line_styled(healthy: bool, tty: bool) -> String {
359    let p = Painter::new(tty);
360    let verdict = if healthy {
361        p.paint_strong(Role::Ok, "HEALTHY")
362    } else {
363        p.paint_strong(Role::Err, "UNHEALTHY")
364    };
365    format!("{}  {}", rail(p), verdict)
366}
367
368/// Section footer (closing box line).
369///
370/// ```text
371/// └──────────────────────────────────────────────
372/// ```
373pub fn section_footer() -> String {
374    section_footer_styled(color_enabled())
375}
376
377fn section_footer_styled(tty: bool) -> String {
378    section_footer_at(tty, width_override())
379}
380
381/// `section_footer_styled` with the width cap given.
382fn section_footer_at(tty: bool, cap: Option<usize>) -> String {
383    let line = rule_at(RULE_WIDTH, 1, cap);
384    Painter::new(tty).paint(Role::Accent, &format!("└{}", line))
385}
386
387/// A separator row inside a section.
388///
389/// ```text
390/// ├──────────────────────────────────────────────
391/// ```
392pub fn separator() -> String {
393    separator_styled(color_enabled())
394}
395
396fn separator_styled(tty: bool) -> String {
397    separator_at(tty, width_override())
398}
399
400/// `separator_styled` with the width cap given.
401fn separator_at(tty: bool, cap: Option<usize>) -> String {
402    let line = rule_at(RULE_WIDTH, 1, cap);
403    Painter::new(tty).paint(Role::Accent, &format!("├{}", line))
404}
405
406/// Format a price with color for peg deviation. The bands come from the
407/// health thresholds: green on the peg, yellow inside the outlier band, red
408/// outside it.
409pub fn format_price_peg(price: f64, peg: &PegBands) -> String {
410    format_price_peg_styled(price, peg, color_enabled())
411}
412
413fn format_price_peg_styled(price: f64, peg: &PegBands, tty: bool) -> String {
414    let role = match peg.classify(price) {
415        PegBand::OnPeg => Role::Ok,
416        PegBand::Slight => Role::Warn,
417        PegBand::Off => Role::Err,
418    };
419    // Decimals come from the peg, so every price of one book lines up.
420    let decimals = crate::display::format::price_decimals(peg.target());
421    Painter::new(tty).paint(role, &format!("{price:.decimals$}"))
422}
423
424/// The USD peg with the default range, for the tests.
425#[cfg(test)]
426fn usd_peg() -> PegBands {
427    crate::market::health::HealthThresholds::default().peg_bands()
428}
429
430/// Empty line with continuation bar.
431pub fn blank_row() -> String {
432    blank_row_styled(color_enabled())
433}
434
435fn blank_row_styled(tty: bool) -> String {
436    rail(Painter::new(tty))
437}
438
439/// The painter the row helpers use: color when stdout takes it.
440pub fn painter() -> Painter {
441    Painter::new(color_enabled())
442}
443
444/// A line of a chart inside a section, after the bar: the line is drawn
445/// and painted already, so it is neither wrapped nor painted again.
446///
447/// ```text
448/// │ 82060.9 ┤ ▄█▄
449/// ```
450pub fn chart_row(line: &str) -> String {
451    format!("{} {line}", rail(painter()))
452}
453
454/// An order book level row with price coloring relative to peg.
455pub fn orderbook_level(
456    price: f64,
457    quantity: f64,
458    base: &str,
459    value: f64,
460    quote: &str,
461    peg: &PegBands,
462) -> String {
463    orderbook_level_styled(price, quantity, base, value, quote, peg, color_enabled())
464}
465
466fn orderbook_level_styled(
467    price: f64,
468    quantity: f64,
469    base: &str,
470    value: f64,
471    quote: &str,
472    peg: &PegBands,
473    tty: bool,
474) -> String {
475    let p = Painter::new(tty);
476    format!(
477        "{}    {}  {:>10.2} {}  {:>10.2} {quote}",
478        rail(p),
479        format_price_peg_styled(price, peg, tty),
480        quantity,
481        p.paint(Role::Secondary, base),
482        value
483    )
484}
485
486// ============================================================================
487// Score / severity / link helpers
488// ============================================================================
489
490/// A visual score bar with color coding.
491///
492/// ```text
493/// │  Security Score   [████████████────────] 60/100
494/// ```
495pub fn score_bar(label: &str, score: u32, max: u32) -> String {
496    score_bar_styled(label, score, max, color_enabled())
497}
498
499fn score_bar_styled(label_text: &str, score: u32, max: u32, tty: bool) -> String {
500    let p = Painter::new(tty);
501    let width = 20usize;
502    let filled = ((score as f64 / max as f64) * width as f64).round() as usize;
503    let filled = filled.min(width);
504    let empty = width - filled;
505    let bar = format!(
506        "[{}{}] {}/{}",
507        "█".repeat(filled),
508        "─".repeat(empty),
509        score,
510        max
511    );
512    let role = if score >= 80 {
513        Role::Ok
514    } else if score >= 50 {
515        Role::Warn
516    } else {
517        Role::Err
518    };
519    format!(
520        "{}  {}{}",
521        rail(p),
522        label(p, label_text),
523        p.paint(role, &bar)
524    )
525}
526
527/// Severity label with color coding.
528///
529/// Returns a colored severity string: Critical (red bold), High (red),
530/// Medium (yellow), Low (cyan), Informational (dimmed).
531pub fn severity_label(severity: &str) -> String {
532    severity_label_styled(severity, color_enabled())
533}
534
535fn severity_label_styled(severity: &str, tty: bool) -> String {
536    let p = Painter::new(tty);
537    match severity.to_lowercase().as_str() {
538        "critical" => p.paint_strong(Role::Err, severity),
539        "high" => p.paint(Role::Err, severity),
540        "medium" => p.paint(Role::Warn, severity),
541        "low" => p.paint(Role::Accent, severity),
542        _ => p.paint(Role::Secondary, severity),
543    }
544}
545
546/// A warning banner inside a section box.
547///
548/// ```text
549/// │  ⚠ WARNING: Source code is NOT verified
550/// ```
551pub fn warning_row(msg: &str) -> String {
552    warning_row_styled(msg, color_enabled())
553}
554
555fn warning_row_styled(msg: &str, tty: bool) -> String {
556    let p = Painter::new(tty);
557    marked_row(p, p.paint_strong(Role::Warn, "⚠"), Role::Warn, msg)
558}
559
560/// An informational note row inside a section box.
561///
562/// ```text
563/// │  ℹ No heuristic findings triggered.
564/// ```
565pub fn info_row(msg: &str) -> String {
566    info_row_styled(msg, color_enabled())
567}
568
569fn info_row_styled(msg: &str, tty: bool) -> String {
570    let p = Painter::new(tty);
571    marked_row(p, p.paint(Role::Info, "ℹ"), Role::Secondary, msg)
572}
573
574/// A link row inside a section box.
575///
576/// ```text
577/// │  Explorer          https://etherscan.io/address/0x...
578/// ```
579pub fn link_row(label: &str, url: &str) -> String {
580    link_row_styled(label, url, color_enabled())
581}
582
583fn link_row_styled(label_text: &str, url: &str, tty: bool) -> String {
584    let p = Painter::new(tty);
585    format!(
586        "{}  {}{}",
587        rail(p),
588        label(p, label_text),
589        p.paint(Role::Link, url)
590    )
591}
592
593/// An indented detail line inside a section (for sub-items like vulnerability descriptions).
594///
595/// ```text
596/// │      Contract source code is not verified.
597/// ```
598pub fn detail_row(msg: &str) -> String {
599    detail_row_styled(msg, color_enabled())
600}
601
602fn detail_row_styled(msg: &str, tty: bool) -> String {
603    let p = Painter::new(tty);
604    // Prefix: "│      " = 7 visible columns
605    let avail = content_width_for(7);
606    let wrapped = wrap_lines(msg, avail);
607
608    let mut out = String::new();
609    for (i, line) in wrapped.iter().enumerate() {
610        if i > 0 {
611            out.push('\n');
612        }
613        out.push_str(&format!(
614            "{}      {}",
615            rail(p),
616            p.paint(Role::Secondary, line)
617        ));
618    }
619    out
620}
621
622/// A bullet-point row inside a section.
623///
624/// ```text
625/// │    • mint (Critical): Can mint tokens
626/// ```
627pub fn bullet_row(msg: &str) -> String {
628    bullet_row_styled(msg, color_enabled())
629}
630
631fn bullet_row_styled(msg: &str, tty: bool) -> String {
632    let p = Painter::new(tty);
633    // Prefix: "│    • " = 7 visible columns
634    let avail = content_width_for(7);
635    let wrapped = wrap_lines(msg, avail);
636
637    let mut out = format!(
638        "{}    {} {}",
639        rail(p),
640        p.paint(Role::Secondary, "•"),
641        wrapped[0]
642    );
643    for line in &wrapped[1..] {
644        out.push_str(&format!("\n{}      {}", rail(p), line));
645    }
646    out
647}
648
649// ============================================================================
650// Table helpers (for columnar data inside sections)
651// ============================================================================
652
653/// Column specification for table formatting.
654pub struct Col<'a> {
655    /// Column header text.
656    pub label: &'a str,
657    /// Minimum width in characters.
658    pub width: usize,
659    /// Alignment: `'<'` for left, `'>'` for right.
660    pub align: char,
661}
662
663/// Format a table header row inside a section box.
664///
665/// ```text
666/// │    Rank      Percent                Balance  Address
667/// │  ─────────────────────────────────────────────────────
668/// ```
669pub fn table_header(cols: &[Col]) -> String {
670    table_header_styled(cols, color_enabled())
671}
672
673fn table_header_styled(cols: &[Col], tty: bool) -> String {
674    table_header_at(cols, tty, width_override())
675}
676
677/// `table_header_styled` with the width cap given.
678fn table_header_at(cols: &[Col], tty: bool, cap: Option<usize>) -> String {
679    let p = Painter::new(tty);
680    let mut header = String::new();
681    for col in cols {
682        if col.align == '>' {
683            header.push_str(&format!("{:>width$}  ", col.label, width = col.width));
684        } else {
685            header.push_str(&format!("{:<width$}  ", col.label, width = col.width));
686        }
687    }
688    let header = header.trim_end().to_string();
689    let rule_len = cols.iter().map(|c| c.width + 2).sum::<usize>();
690    let line = rule_at(rule_len, 3, cap);
691
692    format!(
693        "{}    {}\n{}  {}",
694        rail(p),
695        p.paint_strong(Role::Accent, &header),
696        rail(p),
697        p.paint(Role::Accent, &line)
698    )
699}
700
701/// Format a table data row inside a section box.
702///
703/// Each value is aligned according to the column specification.
704///
705/// ```text
706/// │       1     12.50%         1,000,000  0xdAC1...1ec7
707/// ```
708pub fn table_row(cols: &[Col], values: &[&str]) -> String {
709    table_row_styled(cols, values, color_enabled())
710}
711
712fn table_row_styled(cols: &[Col], values: &[&str], tty: bool) -> String {
713    let mut row = String::new();
714    for (i, col) in cols.iter().enumerate() {
715        let val = values.get(i).copied().unwrap_or("");
716        if col.align == '>' {
717            row.push_str(&format!("{:>width$}  ", val, width = col.width));
718        } else {
719            row.push_str(&format!("{:<width$}  ", val, width = col.width));
720        }
721    }
722    let row = row.trim_end().to_string();
723    format!("{}    {}", rail(Painter::new(tty)), row)
724}
725
726/// Format an enumerated list item inside a section box.
727///
728/// ```text
729/// │  1. Uniswap ETH/USDC - $1.2M ($500K liq)
730/// ```
731pub fn numbered_row(index: usize, msg: &str) -> String {
732    numbered_row_styled(index, msg, color_enabled())
733}
734
735fn numbered_row_styled(index: usize, msg: &str, tty: bool) -> String {
736    let p = Painter::new(tty);
737    // Prefix: "│  N. " = ~6 visible columns (varies with digit count)
738    let prefix_len = 4 + format!("{}.", index).len();
739    let avail = terminal_width().saturating_sub(prefix_len);
740    let wrapped = wrap_lines(msg, avail);
741
742    let num = format!("{}.", index);
743    let mut out = format!(
744        "{}  {} {}",
745        rail(p),
746        p.paint(Role::Secondary, &num),
747        wrapped[0]
748    );
749    let cont_pad = " ".repeat(num.len() + 1);
750    for line in &wrapped[1..] {
751        out.push_str(&format!("\n{}  {}{}", rail(p), cont_pad, line));
752    }
753    out
754}
755
756// ============================================================================
757// Unit Tests
758// ============================================================================
759
760#[cfg(test)]
761mod golden_tests;
762
763#[cfg(test)]
764mod tests {
765    use super::*;
766
767    #[test]
768    fn test_section_header_contains_title() {
769        let header = section_header("Token Health");
770        assert!(header.contains("Token Health"));
771        assert!(header.contains("┌─"));
772    }
773
774    #[test]
775    fn test_subsection_header_contains_title() {
776        let header = subsection_header("DEX Analytics");
777        assert!(header.contains("DEX Analytics"));
778        assert!(header.contains("├──"));
779    }
780
781    #[test]
782    fn test_kv_row_contains_key_value() {
783        let row = kv_row("Price", "$1.00");
784        assert!(row.contains("Price"));
785        assert!(row.contains("$1.00"));
786        assert!(row.contains("│"));
787    }
788
789    #[test]
790    fn test_kv_row_delta_positive() {
791        let row = kv_row_delta("24h Change", 5.0, "+5.00%");
792        assert!(row.contains("+5.00%"));
793    }
794
795    #[test]
796    fn test_kv_row_delta_negative() {
797        let row = kv_row_delta("24h Change", -3.0, "-3.00%");
798        assert!(row.contains("-3.00%"));
799    }
800
801    #[test]
802    fn test_check_pass() {
803        let line = check_pass("No sells below peg");
804        assert!(line.contains("✓"));
805        assert!(line.contains("No sells below peg"));
806    }
807
808    #[test]
809    fn test_check_fail() {
810        let line = check_fail("Bid depth too low");
811        assert!(line.contains("✗"));
812        assert!(line.contains("Bid depth too low"));
813    }
814
815    #[test]
816    fn test_status_line_healthy() {
817        let line = status_line(true);
818        assert!(line.contains("HEALTHY"));
819    }
820
821    #[test]
822    fn test_status_line_unhealthy() {
823        let line = status_line(false);
824        assert!(line.contains("UNHEALTHY"));
825    }
826
827    #[test]
828    fn test_section_footer() {
829        let footer = section_footer();
830        assert!(footer.contains("└"));
831    }
832
833    #[test]
834    fn test_separator() {
835        let sep = separator();
836        assert!(sep.contains("├"));
837    }
838
839    #[test]
840    fn test_format_price_peg_near() {
841        let s = format_price_peg(1.0001, &usd_peg());
842        assert!(s.contains("1.0001"));
843    }
844
845    #[test]
846    fn test_format_price_peg_far() {
847        let s = format_price_peg(0.95, &usd_peg());
848        assert!(s.contains("0.9500"));
849    }
850
851    #[test]
852    fn test_blank_row() {
853        let row = blank_row();
854        assert!(row.contains("│"));
855    }
856
857    #[test]
858    fn test_orderbook_level() {
859        let row = orderbook_level(1.0001, 500.0, "DAI", 500.05, "USDT", &usd_peg());
860        assert!(row.contains("DAI"));
861        assert!(row.contains("USDT"));
862    }
863
864    #[test]
865    fn test_kv_row_delta_zero_value() {
866        // Non-TTY: no color, but zero value (else/dimmed branch) still returns formatted string
867        let row = kv_row_delta("Change", 0.0, "0.00%");
868        assert!(row.contains("Change"));
869        assert!(row.contains("0.00%"));
870        assert!(row.contains("│"));
871    }
872
873    #[test]
874    fn test_format_price_peg_moderate_deviation() {
875        // 0.2% deviation: price 1.002, target 1.0 -> in non-TTY returns plain price string
876        let s = format_price_peg(1.002, &usd_peg());
877        assert!(s.contains("1.0020"));
878    }
879
880    #[test]
881    fn test_orderbook_level_various_prices() {
882        let row_low = orderbook_level(0.9990, 100.0, "DAI", 99.90, "USDT", &usd_peg());
883        let row_mid = orderbook_level(1.0000, 100.0, "DAI", 100.0, "USDT", &usd_peg());
884        let row_high = orderbook_level(1.0015, 100.0, "DAI", 100.15, "USDT", &usd_peg());
885        assert!(row_low.contains("0.9990"));
886        assert!(row_mid.contains("1.0000"));
887        assert!(row_high.contains("1.0015"));
888        assert!(row_low.contains("│"));
889        assert!(row_mid.contains("│"));
890        assert!(row_high.contains("│"));
891    }
892
893    #[test]
894    fn test_non_tty_returns_unicode_box_characters() {
895        // In CI (non-TTY), all helpers still emit Unicode box-drawing chars
896        let header = section_header("Test");
897        let sub = subsection_header("Sub");
898        let kv = kv_row("Key", "Val");
899        let pass = check_pass("ok");
900        let fail = check_fail("err");
901        let footer = section_footer();
902        let sep = separator();
903        let blank = blank_row();
904        let status_healthy = status_line(true);
905        let status_unhealthy = status_line(false);
906
907        assert!(header.contains('┌'), "section_header should contain ┌");
908        assert!(header.contains('─'), "section_header should contain ─");
909        assert!(sub.contains('│'), "subsection_header should contain │");
910        assert!(sub.contains('├'), "subsection_header should contain ├");
911        assert!(kv.contains('│'), "kv_row should contain │");
912        assert!(pass.contains('✓'), "check_pass should contain ✓");
913        assert!(fail.contains('✗'), "check_fail should contain ✗");
914        assert!(footer.contains('└'), "section_footer should contain └");
915        assert!(sep.contains('├'), "separator should contain ├");
916        assert!(blank.contains('│'), "blank_row should contain │");
917        assert!(status_healthy.contains("HEALTHY"));
918        assert!(status_unhealthy.contains("UNHEALTHY"));
919    }
920
921    // ============================================================
922    // TTY-branch tests (via _styled variants with tty=true)
923    // ============================================================
924
925    #[test]
926    fn test_section_header_tty() {
927        let header = section_header_styled("Token Health", true);
928        assert!(header.contains("Token Health"));
929        assert!(header.contains("┌─"));
930    }
931
932    #[test]
933    fn test_subsection_header_tty() {
934        let header = subsection_header_styled("DEX", true);
935        assert!(header.contains("DEX"));
936        assert!(header.contains("├──"));
937    }
938
939    #[test]
940    fn test_kv_row_tty() {
941        let row = kv_row_styled("Price", "$1.00", true);
942        assert!(row.contains("Price"));
943        assert!(row.contains("$1.00"));
944    }
945
946    #[test]
947    fn test_kv_row_delta_positive_tty() {
948        let row = kv_row_delta_styled("Change", 5.0, "+5%", true);
949        assert!(row.contains("+5%"));
950    }
951
952    #[test]
953    fn test_kv_row_delta_negative_tty() {
954        let row = kv_row_delta_styled("Change", -3.0, "-3%", true);
955        assert!(row.contains("-3%"));
956    }
957
958    #[test]
959    fn test_kv_row_delta_zero_tty() {
960        let row = kv_row_delta_styled("Change", 0.0, "0.00%", true);
961        assert!(row.contains("0.00%"));
962    }
963
964    #[test]
965    fn test_check_pass_tty() {
966        let line = check_pass_styled("ok", true);
967        assert!(line.contains("✓"));
968        assert!(line.contains("ok"));
969    }
970
971    #[test]
972    fn test_check_fail_tty() {
973        let line = check_fail_styled("err", true);
974        assert!(line.contains("✗"));
975        assert!(line.contains("err"));
976    }
977
978    #[test]
979    fn test_status_line_healthy_tty() {
980        let line = status_line_styled(true, true);
981        assert!(line.contains("HEALTHY"));
982    }
983
984    #[test]
985    fn test_status_line_unhealthy_tty() {
986        let line = status_line_styled(false, true);
987        assert!(line.contains("UNHEALTHY"));
988    }
989
990    #[test]
991    fn test_section_footer_tty() {
992        let footer = section_footer_styled(true);
993        assert!(footer.contains("└"));
994    }
995
996    #[test]
997    fn test_separator_tty() {
998        let sep = separator_styled(true);
999        assert!(sep.contains("├"));
1000    }
1001
1002    #[test]
1003    fn test_format_price_peg_tty_near() {
1004        let s = format_price_peg_styled(1.0001, &usd_peg(), true);
1005        assert!(s.contains("1.0001"));
1006    }
1007
1008    #[test]
1009    fn test_format_price_peg_tty_moderate() {
1010        let s = format_price_peg_styled(1.003, &usd_peg(), true);
1011        assert!(s.contains("1.0030"));
1012    }
1013
1014    #[test]
1015    fn test_format_price_peg_tty_far() {
1016        let s = format_price_peg_styled(0.95, &usd_peg(), true);
1017        assert!(s.contains("0.9500"));
1018    }
1019
1020    #[test]
1021    fn test_blank_row_tty() {
1022        let row = blank_row_styled(true);
1023        assert!(row.contains("│"));
1024    }
1025
1026    #[test]
1027    fn test_orderbook_level_tty() {
1028        let row = orderbook_level_styled(1.0001, 500.0, "DAI", 500.05, "USDT", &usd_peg(), true);
1029        assert!(row.contains("DAI"));
1030        assert!(row.contains("USDT"));
1031    }
1032
1033    // ============================================================
1034    // Score / severity / link helper tests (non-TTY)
1035    // ============================================================
1036
1037    #[test]
1038    fn test_score_bar_high() {
1039        let bar = score_bar("Security Score", 85, 100);
1040        assert!(bar.contains("85/100"));
1041        assert!(bar.contains("│"));
1042        assert!(bar.contains("█"));
1043    }
1044
1045    #[test]
1046    fn test_score_bar_low() {
1047        let bar = score_bar("Security Score", 20, 100);
1048        assert!(bar.contains("20/100"));
1049    }
1050
1051    #[test]
1052    fn test_score_bar_zero() {
1053        let bar = score_bar("Score", 0, 100);
1054        assert!(bar.contains("0/100"));
1055        assert!(bar.contains("│"));
1056    }
1057
1058    #[test]
1059    fn test_score_bar_max() {
1060        let bar = score_bar("Score", 100, 100);
1061        assert!(bar.contains("100/100"));
1062    }
1063
1064    #[test]
1065    fn test_severity_label_critical() {
1066        let label = severity_label("Critical");
1067        assert!(label.contains("Critical"));
1068    }
1069
1070    #[test]
1071    fn test_severity_label_high() {
1072        let label = severity_label("High");
1073        assert!(label.contains("High"));
1074    }
1075
1076    #[test]
1077    fn test_severity_label_medium() {
1078        let label = severity_label("Medium");
1079        assert!(label.contains("Medium"));
1080    }
1081
1082    #[test]
1083    fn test_severity_label_low() {
1084        let label = severity_label("Low");
1085        assert!(label.contains("Low"));
1086    }
1087
1088    #[test]
1089    fn test_severity_label_informational() {
1090        let label = severity_label("Informational");
1091        assert!(label.contains("Informational"));
1092    }
1093
1094    #[test]
1095    fn test_warning_row() {
1096        let row = warning_row("Source code is NOT verified");
1097        assert!(row.contains("⚠"));
1098        assert!(row.contains("Source code is NOT verified"));
1099        assert!(row.contains("│"));
1100    }
1101
1102    #[test]
1103    fn test_info_row() {
1104        let row = info_row("No findings triggered");
1105        assert!(row.contains("ℹ"));
1106        assert!(row.contains("No findings triggered"));
1107        assert!(row.contains("│"));
1108    }
1109
1110    #[test]
1111    fn test_link_row() {
1112        let row = link_row("Explorer", "https://etherscan.io");
1113        assert!(row.contains("Explorer"));
1114        assert!(row.contains("https://etherscan.io"));
1115        assert!(row.contains("│"));
1116    }
1117
1118    #[test]
1119    fn test_detail_row() {
1120        let row = detail_row("Contract is not verified");
1121        assert!(row.contains("Contract is not verified"));
1122        assert!(row.contains("│"));
1123    }
1124
1125    #[test]
1126    fn test_bullet_row() {
1127        let row = bullet_row("mint (Critical): Can mint tokens");
1128        assert!(row.contains("•"));
1129        assert!(row.contains("mint (Critical): Can mint tokens"));
1130        assert!(row.contains("│"));
1131    }
1132
1133    // ============================================================
1134    // Score / severity / link helper tests (TTY)
1135    // ============================================================
1136
1137    #[test]
1138    fn test_score_bar_tty_high() {
1139        let bar = score_bar_styled("Security Score", 85, 100, true);
1140        assert!(bar.contains("85/100"));
1141        assert!(bar.contains("█"));
1142    }
1143
1144    #[test]
1145    fn test_score_bar_tty_medium() {
1146        let bar = score_bar_styled("Security Score", 55, 100, true);
1147        assert!(bar.contains("55/100"));
1148    }
1149
1150    #[test]
1151    fn test_score_bar_tty_low() {
1152        let bar = score_bar_styled("Security Score", 20, 100, true);
1153        assert!(bar.contains("20/100"));
1154    }
1155
1156    #[test]
1157    fn test_severity_label_tty_critical() {
1158        let label = severity_label_styled("Critical", true);
1159        assert!(label.contains("Critical"));
1160    }
1161
1162    #[test]
1163    fn test_severity_label_tty_low() {
1164        let label = severity_label_styled("Low", true);
1165        assert!(label.contains("Low"));
1166    }
1167
1168    #[test]
1169    fn test_severity_label_tty_unknown() {
1170        let label = severity_label_styled("Unknown", true);
1171        assert!(label.contains("Unknown"));
1172    }
1173
1174    #[test]
1175    fn test_warning_row_tty() {
1176        let row = warning_row_styled("Alert!", true);
1177        assert!(row.contains("⚠"));
1178        assert!(row.contains("Alert!"));
1179    }
1180
1181    #[test]
1182    fn test_info_row_tty() {
1183        let row = info_row_styled("Note", true);
1184        assert!(row.contains("ℹ"));
1185        assert!(row.contains("Note"));
1186    }
1187
1188    #[test]
1189    fn test_link_row_tty() {
1190        let row = link_row_styled("Explorer", "https://example.com", true);
1191        assert!(row.contains("Explorer"));
1192        assert!(row.contains("https://example.com"));
1193    }
1194
1195    #[test]
1196    fn test_detail_row_tty() {
1197        let row = detail_row_styled("Some detail", true);
1198        assert!(row.contains("Some detail"));
1199        assert!(row.contains("│"));
1200    }
1201
1202    #[test]
1203    fn test_bullet_row_tty() {
1204        let row = bullet_row_styled("Item one", true);
1205        assert!(row.contains("•"));
1206        assert!(row.contains("Item one"));
1207    }
1208
1209    // ============================================================
1210    // Word-wrapping engine tests
1211    // ============================================================
1212
1213    #[test]
1214    fn test_wrap_lines_short_text() {
1215        let lines = wrap_lines("hello world", 80);
1216        assert_eq!(lines, vec!["hello world"]);
1217    }
1218
1219    #[test]
1220    fn test_wrap_lines_exact_fit() {
1221        let lines = wrap_lines("abcde fghij", 11);
1222        assert_eq!(lines, vec!["abcde fghij"]);
1223    }
1224
1225    #[test]
1226    fn test_wrap_lines_wraps_at_word_boundary() {
1227        let lines = wrap_lines("hello world foo", 11);
1228        assert_eq!(lines, vec!["hello world", "foo"]);
1229    }
1230
1231    #[test]
1232    fn test_wrap_lines_multiple_wraps() {
1233        let lines = wrap_lines("a b c d e f g", 3);
1234        assert_eq!(lines, vec!["a b", "c d", "e f", "g"]);
1235    }
1236
1237    #[test]
1238    fn test_wrap_lines_long_word_exceeds_width() {
1239        // A single word longer than the width stays on its own line unbroken
1240        let lines = wrap_lines(
1241            "https://etherscan.io/address/0xdAC17F958D2ee523a2206206994597C13D831ec7",
1242            40,
1243        );
1244        assert_eq!(lines.len(), 1);
1245        assert!(lines[0].starts_with("https://"));
1246    }
1247
1248    #[test]
1249    fn test_wrap_lines_long_word_after_short() {
1250        let lines = wrap_lines(
1251            "Explorer: https://etherscan.io/address/0xdAC17F958D2ee523a2206206994597C13D831ec7",
1252            30,
1253        );
1254        assert_eq!(lines.len(), 2);
1255        assert_eq!(lines[0], "Explorer:");
1256        assert!(lines[1].starts_with("https://"));
1257    }
1258
1259    #[test]
1260    fn test_wrap_lines_empty_string() {
1261        let lines = wrap_lines("", 80);
1262        assert_eq!(lines, vec![""]);
1263    }
1264
1265    #[test]
1266    fn test_wrap_lines_zero_width() {
1267        let lines = wrap_lines("hello", 0);
1268        assert_eq!(lines, vec!["hello"]);
1269    }
1270
1271    #[test]
1272    fn test_wrap_lines_single_word() {
1273        let lines = wrap_lines("hello", 80);
1274        assert_eq!(lines, vec!["hello"]);
1275    }
1276
1277    #[test]
1278    fn test_wrap_lines_preserves_all_words() {
1279        let input = "Contract source code is not verified. Full vulnerability analysis requires verified source code.";
1280        let lines = wrap_lines(input, 40);
1281        let reassembled: String = lines.join(" ");
1282        assert_eq!(reassembled, input);
1283    }
1284
1285    #[test]
1286    fn test_terminal_width_returns_positive() {
1287        let _lock = width_lock();
1288        let w = terminal_width();
1289        assert!(w > 0);
1290    }
1291
1292    #[test]
1293    fn test_content_width_for_reasonable_prefix() {
1294        let _lock = width_lock();
1295        let w = content_width_for(7);
1296        // Should be terminal_width - 7; at minimum > 0 even in CI
1297        assert!(w > 0 || terminal_width() <= 7);
1298    }
1299
1300    // ============================================================
1301    // Word-wrapping integration tests (helpers produce multiline)
1302    // ============================================================
1303
1304    #[test]
1305    fn test_detail_row_wraps_long_text() {
1306        let _lock = width_lock();
1307        // detail_row prefix is 7 cols ("│      "); with width=40 that gives 33 cols for content
1308        let long = "Contract source code is not verified and full vulnerability analysis requires verified source code for accurate results";
1309        let row = detail_row_styled(long, false);
1310        // Should contain continuation lines with │ prefix
1311        let line_count = row.lines().count();
1312        // At a typical terminal width this will wrap; in narrow CI it will certainly wrap
1313        assert!(row.contains("│"), "should contain box-drawing prefix");
1314        // All lines should start with │
1315        for line in row.lines() {
1316            assert!(
1317                line.starts_with('│'),
1318                "continuation line should start with │: {}",
1319                line
1320            );
1321        }
1322        // Verify no content is lost
1323        assert!(row.contains("Contract"));
1324        assert!(row.contains("results"));
1325        // If terminal is wide enough, it might not wrap; otherwise verify multiple lines
1326        if terminal_width() < 80 {
1327            assert!(line_count > 1, "should wrap on narrow terminal");
1328        }
1329    }
1330
1331    #[test]
1332    fn test_check_pass_wraps_long_text() {
1333        let _lock = width_lock();
1334        let long = "No sells detected below the configured peg target during the monitoring window across all tracked pairs";
1335        let row = check_pass_styled(long, false);
1336        assert!(row.contains("✓"));
1337        assert!(row.contains("No sells"));
1338        assert!(row.contains("pairs"));
1339        for line in row.lines() {
1340            assert!(line.starts_with('│'));
1341        }
1342    }
1343
1344    #[test]
1345    fn test_check_fail_wraps_long_text() {
1346        let _lock = width_lock();
1347        let long = "Bid depth is significantly below the minimum threshold required for healthy market conditions on this trading pair";
1348        let row = check_fail_styled(long, false);
1349        assert!(row.contains("✗"));
1350        for line in row.lines() {
1351            assert!(line.starts_with('│'));
1352        }
1353    }
1354
1355    #[test]
1356    fn test_warning_row_wraps_long_text() {
1357        let _lock = width_lock();
1358        let long = "Source code is NOT verified — unable to perform source-level analysis. Consider requesting verification.";
1359        let row = warning_row_styled(long, false);
1360        assert!(row.contains("⚠"));
1361        for line in row.lines() {
1362            assert!(line.starts_with('│'));
1363        }
1364    }
1365
1366    #[test]
1367    fn test_info_row_wraps_long_text() {
1368        let _lock = width_lock();
1369        let long = "No audit reports found. Check block explorer and auditor databases manually for third-party audit information.";
1370        let row = info_row_styled(long, false);
1371        assert!(row.contains("ℹ"));
1372        for line in row.lines() {
1373            assert!(line.starts_with('│'));
1374        }
1375    }
1376
1377    #[test]
1378    fn test_bullet_row_wraps_long_text() {
1379        let _lock = width_lock();
1380        let long = "Uniswap V3 integration detected with slippage protection enabled and deadline protection enabled for all swap calls";
1381        let row = bullet_row_styled(long, false);
1382        assert!(row.contains("•"));
1383        for line in row.lines() {
1384            assert!(line.starts_with('│'));
1385        }
1386    }
1387
1388    #[test]
1389    fn test_kv_row_wraps_long_value() {
1390        let _lock = width_lock();
1391        let long_val = "Verified contract with comprehensive access controls and multiple security features including role-based permissions";
1392        let row = kv_row_styled("Summary", long_val, false);
1393        assert!(row.contains("Summary"));
1394        assert!(row.contains("Verified"));
1395        assert!(row.contains("permissions"));
1396        for line in row.lines() {
1397            assert!(line.starts_with('│'));
1398        }
1399    }
1400
1401    #[test]
1402    fn test_kv_row_short_value_no_wrap() {
1403        let _lock = width_lock();
1404        let row = kv_row_styled("Chain", "ethereum", false);
1405        assert_eq!(row.lines().count(), 1);
1406        assert!(row.contains("Chain"));
1407        assert!(row.contains("ethereum"));
1408    }
1409
1410    #[test]
1411    fn test_detail_row_wraps_tty() {
1412        let _lock = width_lock();
1413        let long = "Contract source code is not verified and full vulnerability analysis requires verified source code for accurate results";
1414        let row = detail_row_styled(long, true);
1415        assert!(row.contains("│"));
1416        assert!(row.contains("Contract"));
1417        assert!(row.contains("results"));
1418    }
1419
1420    #[test]
1421    fn test_bullet_row_wraps_tty() {
1422        let _lock = width_lock();
1423        let long = "Uniswap V3 integration detected with slippage protection enabled and deadline protection enabled for all swap calls";
1424        let row = bullet_row_styled(long, true);
1425        assert!(row.contains("•"));
1426        assert!(row.contains("Uniswap"));
1427        assert!(row.contains("calls"));
1428    }
1429
1430    #[test]
1431    fn test_kv_row_wraps_tty() {
1432        let _lock = width_lock();
1433        let long_val = "Verified contract with comprehensive access controls and multiple security features including role-based permissions";
1434        let row = kv_row_styled("Summary", long_val, true);
1435        assert!(row.contains("Summary"));
1436        assert!(row.contains("Verified"));
1437        assert!(row.contains("permissions"));
1438    }
1439
1440    // ============================================================
1441    // Table / numbered row helper tests
1442    // ============================================================
1443
1444    #[test]
1445    fn test_table_header_contains_labels() {
1446        let cols = &[
1447            Col {
1448                label: "Rank",
1449                width: 6,
1450                align: '>',
1451            },
1452            Col {
1453                label: "Name",
1454                width: 20,
1455                align: '<',
1456            },
1457        ];
1458        let header = table_header(cols);
1459        assert!(header.contains("Rank"));
1460        assert!(header.contains("Name"));
1461        assert!(header.contains("│"));
1462        assert!(header.contains("─"));
1463    }
1464
1465    #[test]
1466    fn test_table_row_contains_values() {
1467        let cols = &[
1468            Col {
1469                label: "Rank",
1470                width: 6,
1471                align: '>',
1472            },
1473            Col {
1474                label: "Name",
1475                width: 20,
1476                align: '<',
1477            },
1478        ];
1479        let row = table_row(cols, &["1", "TestToken"]);
1480        assert!(row.contains("1"));
1481        assert!(row.contains("TestToken"));
1482        assert!(row.contains("│"));
1483    }
1484
1485    #[test]
1486    fn test_table_row_missing_values() {
1487        let cols = &[
1488            Col {
1489                label: "A",
1490                width: 5,
1491                align: '<',
1492            },
1493            Col {
1494                label: "B",
1495                width: 5,
1496                align: '<',
1497            },
1498        ];
1499        let row = table_row(cols, &["only"]);
1500        assert!(row.contains("only"));
1501        assert!(row.contains("│"));
1502    }
1503
1504    #[test]
1505    fn test_table_header_tty() {
1506        let cols = &[Col {
1507            label: "Price",
1508            width: 10,
1509            align: '>',
1510        }];
1511        let header = table_header_styled(cols, true);
1512        assert!(header.contains("Price"));
1513        assert!(header.contains("│"));
1514        // The header text is bold cyan: cyan, then bold, then the label.
1515        assert!(header.contains("\x1b[36m\x1b[1m"), "{header:?}");
1516        let plain = table_header_styled(cols, false);
1517        assert!(!plain.contains('\x1b'), "plain output has no escapes");
1518    }
1519
1520    #[test]
1521    fn test_table_row_tty() {
1522        let cols = &[Col {
1523            label: "Price",
1524            width: 10,
1525            align: '>',
1526        }];
1527        let row = table_row_styled(cols, &["$1.00"], true);
1528        assert!(row.contains("$1.00"));
1529        assert!(row.contains("│"));
1530    }
1531
1532    #[test]
1533    fn test_numbered_row_basic() {
1534        let row = numbered_row(1, "First item");
1535        assert!(row.contains("1."));
1536        assert!(row.contains("First item"));
1537        assert!(row.contains("│"));
1538    }
1539
1540    #[test]
1541    fn test_numbered_row_wraps() {
1542        let _lock = width_lock();
1543        let long = "This is a very long description that should eventually wrap to the next line when the terminal width is narrow enough";
1544        let row = numbered_row(1, long);
1545        assert!(row.contains("1."));
1546        assert!(row.contains("This"));
1547        assert!(row.contains("enough"));
1548        for line in row.lines() {
1549            assert!(line.contains('│'));
1550        }
1551    }
1552
1553    #[test]
1554    fn test_numbered_row_tty() {
1555        let row = numbered_row_styled(5, "Fifth item", true);
1556        assert!(row.contains("5."));
1557        assert!(row.contains("Fifth item"));
1558    }
1559
1560    #[test]
1561    fn test_numbered_row_double_digits() {
1562        let row = numbered_row(12, "Twelfth item");
1563        assert!(row.contains("12."));
1564        assert!(row.contains("Twelfth item"));
1565    }
1566
1567    // ── TTY wrapping continuation tests ──
1568    // Exercise the tty=true branch for multi-line wrapping in each helper.
1569
1570    #[test]
1571    fn test_check_pass_wraps_tty() {
1572        let _lock = width_lock();
1573        // Long enough to wrap on any real terminal (up to ~500 columns).
1574        let long = &format!("{} ", "No sells detected below the configured peg target during the monitoring window across all tracked pairs and venues in scope").repeat(4);
1575        let row = check_pass_styled(long, true);
1576        assert!(row.contains("✓"));
1577        assert!(row.lines().count() > 1, "should wrap to multiple lines");
1578    }
1579
1580    #[test]
1581    fn test_check_fail_wraps_tty() {
1582        let _lock = width_lock();
1583        // Long enough to wrap on any real terminal (up to ~500 columns).
1584        let long = &format!("{} ", "Bid depth is significantly below the minimum threshold required for healthy market conditions on this particular trading pair").repeat(4);
1585        let row = check_fail_styled(long, true);
1586        assert!(row.contains("✗"));
1587        assert!(row.lines().count() > 1, "should wrap to multiple lines");
1588    }
1589
1590    #[test]
1591    fn test_check_na_plain_and_tty_wrap() {
1592        let _lock = width_lock();
1593        // Long enough to wrap on any real terminal (up to ~500 columns).
1594        // n/a rows (#37) must read as neither pass nor fail.
1595        let long = &format!("{} ", "Spread: n/a (synthetic AMM book; the real spread is the pool fee, which the data source does not report)").repeat(4);
1596        for tty in [false, true] {
1597            let row = check_na_styled(long, tty);
1598            assert!(row.contains("–"), "tty={}", tty);
1599            assert!(!row.contains("✓") && !row.contains("✗"), "tty={}", tty);
1600            assert!(row.lines().count() > 1, "should wrap, tty={}", tty);
1601        }
1602        assert!(check_na_styled("short", false).starts_with("│  – short"));
1603    }
1604
1605    #[test]
1606    fn test_warning_row_wraps_tty() {
1607        let _lock = width_lock();
1608        // Long enough to wrap on any real terminal (up to ~500 columns).
1609        let long = &format!("{} ", "Source code is NOT verified — unable to perform full source-level analysis on this contract. Consider requesting verification from the deployer.").repeat(4);
1610        let row = warning_row_styled(long, true);
1611        assert!(row.contains("⚠"));
1612        assert!(row.lines().count() > 1, "should wrap to multiple lines");
1613    }
1614
1615    #[test]
1616    fn test_info_row_wraps_tty() {
1617        let _lock = width_lock();
1618        // Long enough to wrap on any real terminal (up to ~500 columns).
1619        let long = &format!("{} ", "No audit reports found in any public database. Check block explorer and auditor databases manually for third-party audit information and verification.").repeat(4);
1620        let row = info_row_styled(long, true);
1621        assert!(row.contains("ℹ"));
1622        assert!(row.lines().count() > 1, "should wrap to multiple lines");
1623    }
1624
1625    #[test]
1626    fn test_numbered_row_wraps_tty() {
1627        let _lock = width_lock();
1628        // Long enough to wrap on any real terminal (up to ~500 columns).
1629        let long = &format!("{} ", "This is a very long description that should eventually wrap to the next line when the terminal width is narrow enough to force it").repeat(4);
1630        let row = numbered_row_styled(1, long, true);
1631        assert!(row.contains("1."));
1632        assert!(row.lines().count() > 1, "should wrap to multiple lines");
1633    }
1634
1635    #[test]
1636    fn test_check_warn_styled_keeps_text_and_continuation_bar() {
1637        let msg = "word ".repeat(60);
1638        let plain = check_warn_styled(&msg, false);
1639        let styled = check_warn_styled(&msg, true);
1640        assert!(plain.starts_with("│  ! "));
1641        assert!(!plain.contains('\x1b'));
1642        assert!(styled.contains('\x1b'), "color on a color-enabled stream");
1643        // Wrapped lines keep the bar, and no text is lost in either mode.
1644        assert!(plain.lines().count() > 1);
1645        assert_eq!(styled.lines().count(), plain.lines().count());
1646        let words = |s: &str| s.matches("word").count();
1647        assert_eq!(words(&plain), 60);
1648        assert_eq!(words(&styled), 60);
1649    }
1650
1651    // ============================================================
1652    // Width override (R11): the TUI sets it to the pane width
1653    // ============================================================
1654
1655    fn widest(text: &str) -> usize {
1656        text.lines().map(UnicodeWidthStr::width).max().unwrap_or(0)
1657    }
1658
1659    fn two_cols() -> [Col<'static>; 2] {
1660        [
1661            Col {
1662                label: "Rank",
1663                width: 150,
1664                align: '<',
1665            },
1666            Col {
1667                label: "Value",
1668                width: 150,
1669                align: '>',
1670            },
1671        ]
1672    }
1673
1674    #[test]
1675    fn test_rules_fit_a_narrow_pane() {
1676        // A rule wider than the pane wraps onto the next row in the TUI.
1677        let cap = Some(40);
1678        assert!(widest(&section_footer_at(false, cap)) <= 40);
1679        assert!(widest(&separator_at(false, cap)) <= 40);
1680        assert!(widest(&section_header_at("Available Venues", false, cap)) <= 40);
1681        let header = table_header_at(&two_cols(), false, cap);
1682        let rule = header.lines().nth(1).unwrap_or_default();
1683        assert_eq!(rule.width(), 40, "{header}");
1684    }
1685
1686    #[test]
1687    fn test_a_wide_pane_keeps_the_fixed_rules() {
1688        // The override only shortens rules; it never makes them longer.
1689        let cap = Some(200);
1690        assert_eq!(section_footer_at(false, cap).width(), 51);
1691        assert_eq!(separator_at(false, cap).width(), 51);
1692        let header = section_header_at("Available Venues", false, cap);
1693        assert_eq!(header.trim().width(), 50);
1694    }
1695
1696    #[test]
1697    fn test_without_an_override_rules_keep_their_fixed_widths() {
1698        // CLI and piped output must not change with the terminal width:
1699        // only the TUI pane (the override) shortens a rule.
1700        let _lock = width_lock();
1701        assert_eq!(width_override(), None);
1702        assert_eq!(section_footer_styled(false).width(), 51);
1703        assert_eq!(separator_styled(false).width(), 51);
1704        let header = section_header_styled("Available Venues", false);
1705        assert_eq!(header.trim().width(), 50);
1706        // Two 150-column cols, wider than any terminal: (150 + 2) * 2 = 304
1707        // `─` after "│  ".
1708        let table = table_header_styled(&two_cols(), false);
1709        assert_eq!(table.lines().nth(1).unwrap_or_default().width(), 3 + 304);
1710        assert_eq!(section_footer_at(false, None), section_footer_styled(false));
1711    }
1712
1713    #[test]
1714    fn test_terminal_width_uses_the_override_first() {
1715        assert_eq!(width_or_real(Some(40)), 40);
1716        let real = crossterm::terminal::size()
1717            .map(|(w, _)| w as usize)
1718            .unwrap_or(80);
1719        assert_eq!(width_or_real(None), real);
1720    }
1721
1722    #[test]
1723    fn test_width_override_set_and_clear() {
1724        // The only test that sets the global. It sets the real width, so
1725        // tests that read terminal_width() without the lock see no change.
1726        let _lock = width_lock();
1727        let real = terminal_width();
1728        set_width_override(Some(real));
1729        assert_eq!(width_override(), Some(real));
1730        set_width_override(Some(0));
1731        assert_eq!(width_override(), None, "Some(0) removes it");
1732        set_width_override(Some(real));
1733        set_width_override(None);
1734        assert_eq!(width_override(), None);
1735        assert_eq!(terminal_width(), real);
1736    }
1737
1738    #[test]
1739    fn test_wrap_lines_counts_display_width_of_cjk() {
1740        // Each CJK char is two columns. Bytes (3 per char) wrap too early;
1741        // chars (1 per char) would put all five words on one 12-column row.
1742        let lines = wrap_lines("日本 語 です", 7);
1743        assert_eq!(lines, vec!["日本 語", "です"]);
1744        for l in &lines {
1745            assert!(l.width() <= 7, "{l}");
1746        }
1747    }
1748}