1
use crate::pane::PaneId;
2

            
3
use super::*;
4

            
5
22
fn console() -> ConsoleState {
6
22
    ConsoleState::new()
7
22
}
8

            
9
1020
fn type_line(state: &mut ConsoleState, text: &str) {
10
5984
    for c in text.chars() {
11
5984
        state.input.insert_char(c);
12
5984
    }
13
1020
}
14

            
15
#[test]
16
1
fn incomplete_form_buffers_and_yields_nothing() {
17
1
    let mut state = console();
18
1
    type_line(&mut state, "(list");
19
1
    assert_eq!(state.take_complete_form(), None);
20
1
    assert_eq!(state.pending, "(list");
21
1
    assert!(state.input.buffer().is_empty());
22
1
    assert!(state.history.is_empty());
23
1
}
24

            
25
#[test]
26
1
fn balanced_form_yields_and_clears_buffer() {
27
1
    let mut state = console();
28
1
    type_line(&mut state, "(+ 1 2)");
29
1
    let form = state.take_complete_form();
30
1
    assert_eq!(form.as_deref(), Some("(+ 1 2)"));
31
1
    assert!(state.pending.is_empty());
32
1
    assert!(state.input.buffer().is_empty());
33
1
}
34

            
35
#[test]
36
1
fn multiline_form_assembles_then_completes() {
37
1
    let mut state = console();
38
1
    type_line(&mut state, "(list");
39
1
    assert_eq!(state.take_complete_form(), None);
40
1
    type_line(&mut state, " 1 2)");
41
1
    let form = state.take_complete_form();
42
1
    assert_eq!(form.as_deref(), Some("(list\n 1 2)"));
43
1
    assert!(state.pending.is_empty());
44
1
}
45

            
46
#[test]
47
1
fn submitted_form_is_pushed_to_history() {
48
1
    let mut state = console();
49
1
    type_line(&mut state, "(foo)");
50
1
    let _ = state.take_complete_form();
51
1
    assert_eq!(state.history, vec!["(foo)".to_string()]);
52
1
    assert_eq!(state.history_cursor, None);
53
1
}
54

            
55
#[test]
56
1
fn incomplete_form_not_pushed_to_history() {
57
1
    let mut state = console();
58
1
    type_line(&mut state, "(open");
59
1
    let _ = state.take_complete_form();
60
1
    assert!(state.history.is_empty());
61
1
}
62

            
63
#[test]
64
1
fn history_prev_loads_latest_then_older_entries() {
65
1
    let mut state = console();
66
3
    for form in ["(a)", "(b)", "(c)"] {
67
3
        type_line(&mut state, form);
68
3
        let _ = state.take_complete_form();
69
3
    }
70
1
    state.history_prev();
71
1
    assert_eq!(state.input.buffer(), "(c)");
72
1
    state.history_prev();
73
1
    assert_eq!(state.input.buffer(), "(b)");
74
1
    state.history_prev();
75
1
    assert_eq!(state.input.buffer(), "(a)");
76
1
    state.history_prev();
77
1
    assert_eq!(state.input.buffer(), "(a)");
78
1
}
79

            
80
#[test]
81
1
fn history_next_walks_forward_then_clears_to_fresh_line() {
82
1
    let mut state = console();
83
2
    for form in ["(a)", "(b)"] {
84
2
        type_line(&mut state, form);
85
2
        let _ = state.take_complete_form();
86
2
    }
87
1
    state.history_prev();
88
1
    state.history_prev();
89
1
    assert_eq!(state.input.buffer(), "(a)");
90
1
    state.history_next();
91
1
    assert_eq!(state.input.buffer(), "(b)");
92
1
    state.history_next();
93
1
    assert!(state.input.buffer().is_empty());
94
1
    assert_eq!(state.history_cursor, None);
95
1
}
96

            
97
#[test]
98
1
fn history_navigation_is_noop_without_history() {
99
1
    let mut state = console();
100
1
    state.history_prev();
101
1
    assert!(state.input.buffer().is_empty());
102
1
    state.history_next();
103
1
    assert!(state.input.buffer().is_empty());
104
1
    assert_eq!(state.history_cursor, None);
105
1
}
106

            
107
#[test]
108
1
fn blank_enter_yields_nothing_and_records_no_history() {
109
1
    let mut state = console();
110
1
    assert_eq!(state.take_complete_form(), None);
111
1
    assert!(state.history.is_empty());
112
1
    assert!(state.pending.is_empty());
113
1
    type_line(&mut state, "   ");
114
1
    assert_eq!(state.take_complete_form(), None);
115
1
    assert!(state.history.is_empty());
116
1
}
117

            
118
#[test]
119
1
fn duplicate_consecutive_submissions_both_recorded() {
120
1
    let mut state = console();
121
1
    for _ in 0..2 {
122
2
        type_line(&mut state, "(a)");
123
2
        assert_eq!(state.take_complete_form().as_deref(), Some("(a)"));
124
    }
125
1
    assert_eq!(state.history, vec!["(a)".to_string(), "(a)".to_string()]);
126
1
}
127

            
128
#[test]
129
1
fn history_next_at_bottom_is_noop() {
130
1
    let mut state = console();
131
1
    type_line(&mut state, "(a)");
132
1
    let _ = state.take_complete_form();
133
1
    assert_eq!(state.history_cursor, None);
134
1
    state.history_next();
135
1
    assert_eq!(state.history_cursor, None);
136
1
    assert!(state.input.buffer().is_empty());
137
1
}
138

            
139
#[test]
140
1
fn history_caps_at_max_dropping_oldest() {
141
1
    let mut state = console();
142
1005
    for i in 0..(MAX_HISTORY_ENTRIES + 5) {
143
1005
        type_line(&mut state, &format!("(f{i})"));
144
1005
        let _ = state.take_complete_form();
145
1005
    }
146
1
    assert_eq!(state.history.len(), MAX_HISTORY_ENTRIES);
147
1
    assert_eq!(state.history.first().unwrap(), "(f5)");
148
1
    assert_eq!(
149
1
        state.history.last().unwrap(),
150
1
        &format!("(f{})", MAX_HISTORY_ENTRIES + 4)
151
    );
152
1
    state.history_prev();
153
1
    assert_eq!(
154
1
        state.input.buffer(),
155
1
        format!("(f{})", MAX_HISTORY_ENTRIES + 4)
156
    );
157
1
}
158

            
159
#[test]
160
1
fn format_result_scalar_number_renders_value() {
161
1
    assert_eq!(format_result("(:id 1 :value 2)"), vec!["2".to_string()]);
162
1
}
163

            
164
#[test]
165
1
fn format_result_scalar_string_renders_value() {
166
1
    assert_eq!(
167
1
        format_result(r#"(:id 1 :value "hello")"#),
168
1
        vec!["hello".to_string()]
169
    );
170
1
}
171

            
172
#[test]
173
1
fn format_result_list_response_aligned_table() {
174
    // A list of two plists: each entity has :name and :symbol fields
175
1
    let envelope =
176
1
        r#"(:id 1 :value "((:name \"Alice\" :symbol \"USD\") (:name \"Bob\" :symbol \"EUR\"))")"#;
177
1
    let lines = format_result(envelope);
178
1
    assert_eq!(lines.len(), 2, "two rows: {lines:?}");
179
1
    assert!(lines[0].contains("Alice"), "{lines:?}");
180
1
    assert!(lines[1].contains("Bob"), "{lines:?}");
181
    // Aligned rows must have equal total length (padding equalises them)
182
1
    assert_eq!(
183
1
        lines[0].len(),
184
1
        lines[1].len(),
185
        "rows not same length: {lines:?}"
186
    );
187
1
}
188

            
189
#[test]
190
1
fn format_result_empty_list_string_verbatim() {
191
    // An empty list is not tabulated — verbatim fallback.
192
1
    let envelope = r#"(:id 1 :value "()")"#;
193
1
    assert_eq!(format_result(envelope), vec!["()".to_string()]);
194
1
}
195

            
196
#[test]
197
1
fn format_result_non_list_string_verbatim() {
198
    // A string that does not read as a list renders verbatim.
199
1
    let envelope = r#"(:id 1 :value "hello world")"#;
200
1
    assert_eq!(format_result(envelope), vec!["hello world".to_string()]);
201
1
}
202

            
203
#[test]
204
1
fn format_result_pair_string_list_tabulated() {
205
    // get-balances returns pair:string — a list whose elements are plist
206
    // strings. It must still tabulate (one line per element), not fall back.
207
1
    let envelope = r#"(:id 1 :value "(\"100 USD\" \"50 EUR\")")"#;
208
1
    let lines = format_result(envelope);
209
1
    assert_eq!(lines.len(), 2, "{lines:?}");
210
1
    assert!(lines.iter().any(|l| l.contains("USD")), "{lines:?}");
211
2
    assert!(lines.iter().any(|l| l.contains("EUR")), "{lines:?}");
212
1
}
213

            
214
#[test]
215
1
fn format_result_record_list_with_leading_whitespace_tabulated() {
216
1
    let envelope = r#"(:id 1 :value "  ((:name \"Alice\" :symbol \"USD\"))")"#;
217
1
    let lines = format_result(envelope);
218
1
    assert_eq!(lines.len(), 1, "{lines:?}");
219
1
    assert!(lines[0].contains("Alice"), "{lines:?}");
220
1
}
221

            
222
#[test]
223
1
fn format_result_error_envelope_renders_error_prefix() {
224
1
    let envelope = r#"(:id 1 :error (:code not-found :message "account missing"))"#;
225
1
    let lines = format_result(envelope);
226
1
    assert_eq!(lines.len(), 1, "{lines:?}");
227
1
    assert_eq!(lines[0], "[error] not-found: account missing");
228
1
}
229

            
230
#[test]
231
1
fn format_result_request_envelope_falls_back_verbatim() {
232
    // Request envelopes are not response frames; parse_wire errors → verbatim
233
1
    let envelope = "(:id 0 :form (foo))";
234
1
    assert_eq!(format_result(envelope), vec![envelope.to_string()]);
235
1
}
236

            
237
#[test]
238
1
fn format_result_plain_text_falls_back_verbatim() {
239
1
    assert_eq!(
240
1
        format_result("line one\nline two"),
241
1
        vec!["line one".to_string(), "line two".to_string()]
242
    );
243
1
}
244

            
245
#[test]
246
1
fn format_result_worker_stopped_notice_verbatim() {
247
1
    let notice = "eval worker stopped";
248
1
    assert_eq!(format_result(notice), vec![notice.to_string()]);
249
1
}
250

            
251
#[test]
252
1
fn align_rows_empty() {
253
1
    assert!(align_rows(&[]).is_empty());
254
1
}
255

            
256
#[test]
257
1
fn align_rows_single_column() {
258
1
    let rows = vec![vec!["a".to_string()], vec!["bbb".to_string()]];
259
1
    let out = align_rows(&rows);
260
1
    assert_eq!(out, vec!["a", "bbb"]);
261
1
}
262

            
263
#[test]
264
1
fn align_rows_multiple_columns_pads_to_max_width() {
265
1
    let rows = vec![
266
1
        vec!["hi".to_string(), "there".to_string()],
267
1
        vec!["goodbye".to_string(), "ok".to_string()],
268
    ];
269
1
    let out = align_rows(&rows);
270
1
    assert_eq!(out[0], "hi       there");
271
1
    assert_eq!(out[1], "goodbye  ok");
272
1
}
273

            
274
#[test]
275
1
fn align_rows_ragged_rows() {
276
1
    let rows = vec![
277
1
        vec!["a".to_string(), "b".to_string(), "c".to_string()],
278
1
        vec!["longer".to_string()],
279
    ];
280
1
    let out = align_rows(&rows);
281
1
    assert_eq!(out.len(), 2);
282
    // Second row has only one cell; still renders without panic
283
1
    assert!(out[1].starts_with("longer"));
284
1
}
285

            
286
#[test]
287
1
fn align_rows_trailing_whitespace_trimmed() {
288
1
    let rows = vec![vec!["x".to_string()]];
289
1
    let out = align_rows(&rows);
290
1
    assert_eq!(out[0], "x");
291
1
}
292

            
293
#[test]
294
1
fn format_result_nil_value_renders_nil() {
295
    // NIL scalar value → "NIL"
296
1
    let lines = format_result("(:id 1 :value NIL)");
297
1
    assert_eq!(lines, vec!["NIL".to_string()]);
298
1
}
299

            
300
#[test]
301
1
fn scroll_up_increases_offset_clamped_to_len() {
302
1
    let mut state = console();
303
5
    for i in 0..5 {
304
5
        state.push_scrollback(format!("line {i}"));
305
5
    }
306
1
    state.scroll_up(3);
307
1
    assert_eq!(state.scroll, 3);
308
    // Clamped: cannot exceed scrollback.len()
309
1
    state.scroll_up(100);
310
1
    assert_eq!(state.scroll, state.scrollback.len());
311
1
}
312

            
313
#[test]
314
1
fn scroll_down_decreases_offset_saturating_at_zero() {
315
1
    let mut state = console();
316
5
    for i in 0..5 {
317
5
        state.push_scrollback(format!("line {i}"));
318
5
    }
319
1
    state.scroll_up(3);
320
1
    state.scroll_down(1);
321
1
    assert_eq!(state.scroll, 2);
322
1
    state.scroll_down(100);
323
1
    assert_eq!(state.scroll, 0);
324
1
}
325

            
326
#[test]
327
1
fn reset_scroll_returns_to_zero() {
328
1
    let mut state = console();
329
5
    for i in 0..5 {
330
5
        state.push_scrollback(format!("line {i}"));
331
5
    }
332
1
    state.scroll_up(3);
333
1
    state.reset_scroll();
334
1
    assert_eq!(state.scroll, 0);
335
1
}
336

            
337
#[test]
338
1
fn visible_scrollback_at_zero_shows_tail() {
339
1
    let mut state = console();
340
10
    for i in 0..10 {
341
10
        state.push_scrollback(format!("line {i}"));
342
10
    }
343
1
    let visible = state.visible_scrollback(3);
344
1
    assert_eq!(visible, &["line 7", "line 8", "line 9"]);
345
1
}
346

            
347
#[test]
348
1
fn visible_scrollback_scrolled_up_shows_older_lines() {
349
1
    let mut state = console();
350
10
    for i in 0..10 {
351
10
        state.push_scrollback(format!("line {i}"));
352
10
    }
353
1
    state.scroll_up(3);
354
    // end = 10 - 3 = 7, start = 7 - 3 = 4
355
1
    let visible = state.visible_scrollback(3);
356
1
    assert_eq!(visible, &["line 4", "line 5", "line 6"]);
357
    // Newest line is not visible
358
1
    assert!(!visible.contains(&"line 9".to_string()));
359
1
}
360

            
361
#[test]
362
1
fn visible_scrollback_over_scrolled_clamps_to_oldest_window() {
363
1
    let mut state = console();
364
5
    for i in 0..5 {
365
5
        state.push_scrollback(format!("line {i}"));
366
5
    }
367
1
    state.scroll_up(5);
368
1
    assert_eq!(state.scroll, 5);
369
    // Stored offset (5) exceeds len - height (2); effective offset clamps so
370
    // the oldest 3 lines stay visible rather than emptying the window.
371
1
    let visible = state.visible_scrollback(3);
372
1
    assert_eq!(visible, &state.scrollback[0..3]);
373
1
    assert_eq!(visible, &["line 0", "line 1", "line 2"]);
374
1
    assert!(!visible.is_empty());
375
1
}
376

            
377
#[test]
378
1
fn visible_scrollback_height_larger_than_buffer_shows_all() {
379
1
    let mut state = console();
380
1
    state.push_scrollback("only");
381
1
    let visible = state.visible_scrollback(100);
382
1
    assert_eq!(visible, &["only"]);
383
1
}
384

            
385
#[test]
386
1
fn visible_scrollback_empty_scrollback_is_empty() {
387
1
    let state = console();
388
1
    assert!(state.visible_scrollback(10).is_empty());
389
1
}
390

            
391
#[test]
392
1
fn default_pane_is_prompt() {
393
1
    let state = console();
394
1
    assert_eq!(state.panes.focused(), PaneId::Prompt);
395
1
}
396

            
397
#[test]
398
1
fn push_scrollback_caps_at_max_dropping_oldest() {
399
1
    let mut state = console();
400
1005
    for i in 0..(MAX_SCROLLBACK_LINES + 5) {
401
1005
        state.push_scrollback(format!("line {i}"));
402
1005
    }
403
1
    assert_eq!(state.scrollback.len(), MAX_SCROLLBACK_LINES);
404
1
    assert_eq!(state.scrollback.first().unwrap(), "line 5");
405
1
    assert_eq!(
406
1
        state.scrollback.last().unwrap(),
407
1
        &format!("line {}", MAX_SCROLLBACK_LINES + 4)
408
    );
409
1
}