Skip to main content

aprender_test_showcase/tui/
app.rs

1//! TUI Application State
2//!
3//! Probar: Error prevention - State machine prevents invalid transitions
4
5use crate::core::evaluator::Evaluator;
6use crate::core::history::History;
7use crate::core::{AnomalyValidator, CalcError};
8
9/// Calculator application state
10#[derive(Debug)]
11pub struct CalculatorApp {
12    /// Current input expression
13    input: String,
14    /// Current cursor position in input
15    cursor: usize,
16    /// Last calculation result
17    result: Option<Result<f64, CalcError>>,
18    /// Calculation history
19    history: History,
20    /// Expression evaluator
21    evaluator: Evaluator,
22    /// Whether the app should quit
23    should_quit: bool,
24}
25
26impl Default for CalculatorApp {
27    fn default() -> Self {
28        Self::new()
29    }
30}
31
32impl CalculatorApp {
33    /// Creates a new calculator app with default settings
34    #[must_use]
35    pub fn new() -> Self {
36        Self {
37            input: String::new(),
38            cursor: 0,
39            result: None,
40            history: History::new(),
41            evaluator: Evaluator::new(),
42            should_quit: false,
43        }
44    }
45
46    /// Creates a calculator app with custom Anomaly validator
47    #[must_use]
48    pub fn with_validator(validator: AnomalyValidator) -> Self {
49        Self {
50            input: String::new(),
51            cursor: 0,
52            result: None,
53            history: History::new(),
54            evaluator: Evaluator::with_validator(validator),
55            should_quit: false,
56        }
57    }
58
59    /// Returns the current input string
60    #[must_use]
61    pub fn input(&self) -> &str {
62        &self.input
63    }
64
65    /// Returns the cursor position
66    #[must_use]
67    pub fn cursor(&self) -> usize {
68        self.cursor
69    }
70
71    /// Returns the last result
72    #[must_use]
73    pub fn result(&self) -> Option<&Result<f64, CalcError>> {
74        self.result.as_ref()
75    }
76
77    /// Returns the calculation history
78    #[must_use]
79    pub fn history(&self) -> &History {
80        &self.history
81    }
82
83    /// Returns whether the app should quit
84    #[must_use]
85    pub fn should_quit(&self) -> bool {
86        self.should_quit
87    }
88
89    /// Sets the quit flag
90    pub fn quit(&mut self) {
91        self.should_quit = true;
92    }
93
94    /// Sets the input string directly (for testing)
95    pub fn set_input(&mut self, input: &str) {
96        self.input = input.to_string();
97        self.cursor = self.input.len();
98    }
99
100    /// Sets the cursor position directly (for testing)
101    pub fn set_cursor(&mut self, pos: usize) {
102        self.cursor = pos.min(self.input.len());
103    }
104
105    /// Inserts a character at the cursor position
106    pub fn insert_char(&mut self, c: char) {
107        self.input.insert(self.cursor, c);
108        self.cursor += 1;
109    }
110
111    /// Deletes the character before the cursor (backspace)
112    pub fn delete_char(&mut self) {
113        if self.cursor > 0 {
114            self.cursor -= 1;
115            self.input.remove(self.cursor);
116        }
117    }
118
119    /// Deletes the character at the cursor (delete key)
120    pub fn delete_char_forward(&mut self) {
121        if self.cursor < self.input.len() {
122            self.input.remove(self.cursor);
123        }
124    }
125
126    /// Moves the cursor left
127    pub fn move_cursor_left(&mut self) {
128        if self.cursor > 0 {
129            self.cursor -= 1;
130        }
131    }
132
133    /// Moves the cursor right
134    pub fn move_cursor_right(&mut self) {
135        if self.cursor < self.input.len() {
136            self.cursor += 1;
137        }
138    }
139
140    /// Moves cursor to the beginning of input
141    pub fn move_cursor_start(&mut self) {
142        self.cursor = 0;
143    }
144
145    /// Moves cursor to the end of input
146    pub fn move_cursor_end(&mut self) {
147        self.cursor = self.input.len();
148    }
149
150    /// Clears the input
151    pub fn clear(&mut self) {
152        self.input.clear();
153        self.cursor = 0;
154        self.result = None;
155    }
156
157    /// Clears everything including history
158    pub fn clear_all(&mut self) {
159        self.clear();
160        self.history.clear();
161    }
162
163    /// Evaluates the current input expression
164    pub fn evaluate(&mut self) {
165        if self.input.is_empty() {
166            return;
167        }
168
169        let result = self.evaluator.evaluate_str(&self.input);
170
171        // Record successful calculations in history
172        if let Ok(value) = &result {
173            self.history.record(&self.input, *value);
174        }
175
176        self.result = Some(result);
177    }
178
179    /// Returns the result as a display string
180    #[must_use]
181    pub fn result_display(&self) -> String {
182        match &self.result {
183            None => String::new(),
184            Some(Ok(value)) => format_result(*value),
185            Some(Err(e)) => format!("Error: {e}"),
186        }
187    }
188
189    /// Loads the last history entry into input
190    pub fn recall_last(&mut self) {
191        if let Some(entry) = self.history.last() {
192            self.input = entry.expression.clone();
193            self.cursor = self.input.len();
194        }
195    }
196
197    /// Gets the Anomaly status as a display string
198    #[must_use]
199    pub fn jidoka_status(&self) -> Vec<String> {
200        let mut status = Vec::new();
201
202        match &self.result {
203            None => {
204                status.push("Ready".into());
205            }
206            Some(Ok(_)) => {
207                status.push("✓ No NaN detected".into());
208                status.push("✓ No overflow".into());
209                status.push("✓ All invariants satisfied".into());
210            }
211            Some(Err(CalcError::AnomalyViolation(v))) => {
212                status.push(format!("✗ Anomaly violation: {v}"));
213            }
214            Some(Err(e)) => {
215                status.push(format!("✗ Error: {e}"));
216            }
217        }
218
219        status
220    }
221}
222
223/// Formats a result value for display
224fn format_result(value: f64) -> String {
225    if value.fract() == 0.0 && value.abs() < 1e15 {
226        format!("{:.0}", value)
227    } else {
228        // Limit decimal places for readability
229        let formatted = format!("{:.10}", value);
230        // Trim trailing zeros
231        let trimmed = formatted.trim_end_matches('0').trim_end_matches('.');
232        trimmed.to_string()
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    // ===== Constructor tests =====
241
242    #[test]
243    fn test_app_new() {
244        let app = CalculatorApp::new();
245        assert!(app.input().is_empty());
246        assert_eq!(app.cursor(), 0);
247        assert!(app.result().is_none());
248        assert!(app.history().is_empty());
249        assert!(!app.should_quit());
250    }
251
252    #[test]
253    fn test_app_default() {
254        let app = CalculatorApp::default();
255        assert!(app.input().is_empty());
256    }
257
258    #[test]
259    fn test_app_with_validator() {
260        let validator = AnomalyValidator::with_max_magnitude(100.0);
261        let mut app = CalculatorApp::with_validator(validator);
262        app.set_input("50 * 3");
263        app.evaluate();
264        assert!(matches!(
265            app.result(),
266            Some(Err(CalcError::AnomalyViolation(_)))
267        ));
268    }
269
270    // ===== Input manipulation tests =====
271
272    #[test]
273    fn test_set_input() {
274        let mut app = CalculatorApp::new();
275        app.set_input("2 + 2");
276        assert_eq!(app.input(), "2 + 2");
277        assert_eq!(app.cursor(), 5);
278    }
279
280    #[test]
281    fn test_insert_char() {
282        let mut app = CalculatorApp::new();
283        app.insert_char('1');
284        app.insert_char('+');
285        app.insert_char('2');
286        assert_eq!(app.input(), "1+2");
287        assert_eq!(app.cursor(), 3);
288    }
289
290    #[test]
291    fn test_insert_char_in_middle() {
292        let mut app = CalculatorApp::new();
293        app.set_input("12");
294        app.set_cursor(1); // Between 1 and 2
295        app.insert_char('+');
296        assert_eq!(app.input(), "1+2");
297    }
298
299    #[test]
300    fn test_delete_char() {
301        let mut app = CalculatorApp::new();
302        app.set_input("123");
303        app.delete_char();
304        assert_eq!(app.input(), "12");
305        assert_eq!(app.cursor(), 2);
306    }
307
308    #[test]
309    fn test_delete_char_at_start() {
310        let mut app = CalculatorApp::new();
311        app.set_input("123");
312        app.set_cursor(0);
313        app.delete_char(); // Should do nothing
314        assert_eq!(app.input(), "123");
315    }
316
317    #[test]
318    fn test_delete_char_forward() {
319        let mut app = CalculatorApp::new();
320        app.set_input("123");
321        app.set_cursor(0);
322        app.delete_char_forward();
323        assert_eq!(app.input(), "23");
324    }
325
326    #[test]
327    fn test_delete_char_forward_at_end() {
328        let mut app = CalculatorApp::new();
329        app.set_input("123");
330        app.delete_char_forward(); // Cursor at end, nothing to delete
331        assert_eq!(app.input(), "123");
332    }
333
334    // ===== Cursor movement tests =====
335
336    #[test]
337    fn test_move_cursor_left() {
338        let mut app = CalculatorApp::new();
339        app.set_input("123");
340        app.move_cursor_left();
341        assert_eq!(app.cursor(), 2);
342    }
343
344    #[test]
345    fn test_move_cursor_left_at_start() {
346        let mut app = CalculatorApp::new();
347        app.set_input("123");
348        app.set_cursor(0);
349        app.move_cursor_left(); // Should stay at 0
350        assert_eq!(app.cursor(), 0);
351    }
352
353    #[test]
354    fn test_move_cursor_right() {
355        let mut app = CalculatorApp::new();
356        app.set_input("123");
357        app.set_cursor(0);
358        app.move_cursor_right();
359        assert_eq!(app.cursor(), 1);
360    }
361
362    #[test]
363    fn test_move_cursor_right_at_end() {
364        let mut app = CalculatorApp::new();
365        app.set_input("123");
366        app.move_cursor_right(); // Already at end
367        assert_eq!(app.cursor(), 3);
368    }
369
370    #[test]
371    fn test_move_cursor_start() {
372        let mut app = CalculatorApp::new();
373        app.set_input("123");
374        app.move_cursor_start();
375        assert_eq!(app.cursor(), 0);
376    }
377
378    #[test]
379    fn test_move_cursor_end() {
380        let mut app = CalculatorApp::new();
381        app.set_input("123");
382        app.set_cursor(0);
383        app.move_cursor_end();
384        assert_eq!(app.cursor(), 3);
385    }
386
387    // ===== Clear tests =====
388
389    #[test]
390    fn test_clear() {
391        let mut app = CalculatorApp::new();
392        app.set_input("2 + 2");
393        app.evaluate();
394        app.clear();
395        assert!(app.input().is_empty());
396        assert_eq!(app.cursor(), 0);
397        assert!(app.result().is_none());
398        // History should be preserved
399        assert!(!app.history().is_empty());
400    }
401
402    #[test]
403    fn test_clear_all() {
404        let mut app = CalculatorApp::new();
405        app.set_input("2 + 2");
406        app.evaluate();
407        app.clear_all();
408        assert!(app.input().is_empty());
409        assert!(app.history().is_empty());
410    }
411
412    // ===== Evaluation tests =====
413
414    #[test]
415    fn test_evaluate_simple() {
416        let mut app = CalculatorApp::new();
417        app.set_input("2 + 3");
418        app.evaluate();
419        assert_eq!(app.result(), Some(&Ok(5.0)));
420    }
421
422    #[test]
423    fn test_evaluate_empty() {
424        let mut app = CalculatorApp::new();
425        app.evaluate(); // Empty input
426        assert!(app.result().is_none());
427    }
428
429    #[test]
430    fn test_evaluate_error() {
431        let mut app = CalculatorApp::new();
432        app.set_input("1 / 0");
433        app.evaluate();
434        assert!(matches!(app.result(), Some(Err(CalcError::DivisionByZero))));
435    }
436
437    #[test]
438    fn test_evaluate_records_history() {
439        let mut app = CalculatorApp::new();
440        app.set_input("2 + 2");
441        app.evaluate();
442        assert_eq!(app.history().len(), 1);
443        assert_eq!(app.history().last().unwrap().result, 4.0);
444    }
445
446    #[test]
447    fn test_evaluate_error_not_in_history() {
448        let mut app = CalculatorApp::new();
449        app.set_input("1 / 0");
450        app.evaluate();
451        assert!(app.history().is_empty());
452    }
453
454    // ===== Result display tests =====
455
456    #[test]
457    fn test_result_display_none() {
458        let app = CalculatorApp::new();
459        assert_eq!(app.result_display(), "");
460    }
461
462    #[test]
463    fn test_result_display_integer() {
464        let mut app = CalculatorApp::new();
465        app.set_input("2 + 2");
466        app.evaluate();
467        assert_eq!(app.result_display(), "4");
468    }
469
470    #[test]
471    fn test_result_display_decimal() {
472        let mut app = CalculatorApp::new();
473        app.set_input("1 / 3");
474        app.evaluate();
475        let display = app.result_display();
476        assert!(display.starts_with("0.333"));
477    }
478
479    #[test]
480    fn test_result_display_error() {
481        let mut app = CalculatorApp::new();
482        app.set_input("1 / 0");
483        app.evaluate();
484        assert!(app.result_display().contains("Error"));
485    }
486
487    // ===== Quit tests =====
488
489    #[test]
490    fn test_quit() {
491        let mut app = CalculatorApp::new();
492        assert!(!app.should_quit());
493        app.quit();
494        assert!(app.should_quit());
495    }
496
497    // ===== History recall tests =====
498
499    #[test]
500    fn test_recall_last() {
501        let mut app = CalculatorApp::new();
502        app.set_input("5 * 5");
503        app.evaluate();
504        app.clear();
505        app.recall_last();
506        assert_eq!(app.input(), "5 * 5");
507    }
508
509    #[test]
510    fn test_recall_last_empty_history() {
511        let mut app = CalculatorApp::new();
512        app.recall_last(); // Should do nothing
513        assert!(app.input().is_empty());
514    }
515
516    // ===== Anomaly status tests =====
517
518    #[test]
519    fn test_jidoka_status_ready() {
520        let app = CalculatorApp::new();
521        let status = app.jidoka_status();
522        assert!(status.contains(&"Ready".to_string()));
523    }
524
525    #[test]
526    fn test_jidoka_status_success() {
527        let mut app = CalculatorApp::new();
528        app.set_input("2 + 2");
529        app.evaluate();
530        let status = app.jidoka_status();
531        assert!(status.iter().any(|s| s.contains("✓")));
532    }
533
534    #[test]
535    fn test_jidoka_status_violation() {
536        let validator = AnomalyValidator::with_max_magnitude(10.0);
537        let mut app = CalculatorApp::with_validator(validator);
538        app.set_input("5 * 5");
539        app.evaluate();
540        let status = app.jidoka_status();
541        assert!(status.iter().any(|s| s.contains("✗")));
542    }
543
544    #[test]
545    fn test_jidoka_status_error() {
546        let mut app = CalculatorApp::new();
547        app.set_input("1 / 0");
548        app.evaluate();
549        let status = app.jidoka_status();
550        assert!(status.iter().any(|s| s.contains("Error")));
551    }
552
553    // ===== Format result tests =====
554
555    #[test]
556    fn test_format_result_integer() {
557        assert_eq!(format_result(42.0), "42");
558    }
559
560    #[test]
561    fn test_format_result_negative_integer() {
562        assert_eq!(format_result(-42.0), "-42");
563    }
564
565    #[test]
566    fn test_format_result_decimal() {
567        assert_eq!(format_result(3.14), "3.14");
568    }
569
570    #[test]
571    fn test_format_result_trailing_zeros() {
572        assert_eq!(format_result(1.50), "1.5");
573    }
574
575    #[test]
576    fn test_format_result_large_integer() {
577        assert_eq!(format_result(1e14), "100000000000000");
578    }
579
580    #[test]
581    fn test_format_result_very_large() {
582        // Large number should not be formatted as integer
583        let result = format_result(1e16);
584        assert!(result.contains('e') || result.len() > 15);
585    }
586}