1
use crate::types::{AccountSuggestion, CommoditySuggestion, TagSuggestion};
2

            
3
pub trait Suggestion: Clone + serde::de::DeserializeOwned + 'static {
4
    fn display_text(&self) -> &str;
5
    fn matches(&self, query: &str) -> bool;
6
    fn get_id(&self) -> &str;
7
    fn get_secondary_value(&self) -> Option<&str>;
8
}
9

            
10
impl Suggestion for AccountSuggestion {
11
    fn display_text(&self) -> &str {
12
        &self.name
13
    }
14

            
15
4
    fn matches(&self, query: &str) -> bool {
16
4
        self.name.to_lowercase().contains(&query.to_lowercase())
17
4
    }
18

            
19
1
    fn get_id(&self) -> &str {
20
1
        &self.id
21
1
    }
22

            
23
2
    fn get_secondary_value(&self) -> Option<&str> {
24
2
        self.currency.as_deref()
25
2
    }
26
}
27

            
28
impl Suggestion for CommoditySuggestion {
29
1
    fn display_text(&self) -> &str {
30
1
        &self.symbol
31
1
    }
32

            
33
5
    fn matches(&self, query: &str) -> bool {
34
5
        let q = query.to_lowercase();
35
5
        self.symbol.to_lowercase().contains(&q) || self.name.to_lowercase().contains(&q)
36
5
    }
37

            
38
1
    fn get_id(&self) -> &str {
39
1
        &self.id
40
1
    }
41

            
42
1
    fn get_secondary_value(&self) -> Option<&str> {
43
1
        Some(&self.id)
44
1
    }
45
}
46

            
47
impl Suggestion for TagSuggestion {
48
    fn display_text(&self) -> &str {
49
        &self.name
50
    }
51

            
52
6
    fn matches(&self, query: &str) -> bool {
53
6
        let q = query.to_lowercase();
54
6
        self.name.to_lowercase().contains(&q)
55
3
            || self
56
3
                .value
57
3
                .as_ref()
58
3
                .is_some_and(|v| v.to_lowercase().contains(&q))
59
6
    }
60

            
61
1
    fn get_id(&self) -> &str {
62
1
        &self.name
63
1
    }
64

            
65
1
    fn get_secondary_value(&self) -> Option<&str> {
66
1
        self.value.as_deref()
67
1
    }
68
}
69

            
70
#[cfg(test)]
71
mod tests {
72
    use super::*;
73

            
74
    #[test]
75
1
    fn account_suggestion_matches_case_insensitive() {
76
1
        let suggestion = AccountSuggestion {
77
1
            id: "123".into(),
78
1
            name: "Checking Account".into(),
79
1
            currency: Some("456".into()),
80
1
        };
81
1
        assert!(suggestion.matches("checking"));
82
1
        assert!(suggestion.matches("ACCOUNT"));
83
1
        assert!(suggestion.matches("Checking"));
84
1
        assert!(!suggestion.matches("savings"));
85
1
    }
86

            
87
    #[test]
88
1
    fn account_suggestion_returns_correct_id() {
89
1
        let suggestion = AccountSuggestion {
90
1
            id: "acc-123".into(),
91
1
            name: "Test".into(),
92
1
            currency: Some("cur-456".into()),
93
1
        };
94
1
        assert_eq!(suggestion.get_id(), "acc-123");
95
1
        assert_eq!(suggestion.get_secondary_value(), Some("cur-456"));
96
1
    }
97

            
98
    #[test]
99
1
    fn account_suggestion_handles_none_currency() {
100
1
        let suggestion = AccountSuggestion {
101
1
            id: "acc-123".into(),
102
1
            name: "Test".into(),
103
1
            currency: None,
104
1
        };
105
1
        assert_eq!(suggestion.get_secondary_value(), None);
106
1
    }
107

            
108
    #[test]
109
1
    fn commodity_matches_symbol_or_name() {
110
1
        let suggestion = CommoditySuggestion {
111
1
            id: "com-123".into(),
112
1
            symbol: "USD".into(),
113
1
            name: "US Dollar".into(),
114
1
        };
115
1
        assert!(suggestion.matches("usd"));
116
1
        assert!(suggestion.matches("USD"));
117
1
        assert!(suggestion.matches("dollar"));
118
1
        assert!(suggestion.matches("US"));
119
1
        assert!(!suggestion.matches("euro"));
120
1
    }
121

            
122
    #[test]
123
1
    fn commodity_returns_symbol_as_display_text() {
124
1
        let suggestion = CommoditySuggestion {
125
1
            id: "com-123".into(),
126
1
            symbol: "EUR".into(),
127
1
            name: "Euro".into(),
128
1
        };
129
1
        assert_eq!(suggestion.display_text(), "EUR");
130
1
    }
131

            
132
    #[test]
133
1
    fn commodity_returns_id_as_secondary() {
134
1
        let suggestion = CommoditySuggestion {
135
1
            id: "com-456".into(),
136
1
            symbol: "GBP".into(),
137
1
            name: "British Pound".into(),
138
1
        };
139
1
        assert_eq!(suggestion.get_id(), "com-456");
140
1
        assert_eq!(suggestion.get_secondary_value(), Some("com-456"));
141
1
    }
142

            
143
    #[test]
144
1
    fn tag_matches_name_or_value() {
145
1
        let suggestion = TagSuggestion {
146
1
            name: "category".into(),
147
1
            value: Some("groceries".into()),
148
1
        };
149
1
        assert!(suggestion.matches("cat"));
150
1
        assert!(suggestion.matches("CATEGORY"));
151
1
        assert!(suggestion.matches("groc"));
152
1
        assert!(!suggestion.matches("transport"));
153
1
    }
154

            
155
    #[test]
156
1
    fn tag_matches_name_only_when_no_value() {
157
1
        let suggestion = TagSuggestion {
158
1
            name: "status".into(),
159
1
            value: None,
160
1
        };
161
1
        assert!(suggestion.matches("stat"));
162
1
        assert!(!suggestion.matches("value"));
163
1
    }
164

            
165
    #[test]
166
1
    fn tag_returns_name_as_id() {
167
1
        let suggestion = TagSuggestion {
168
1
            name: "priority".into(),
169
1
            value: Some("high".into()),
170
1
        };
171
1
        assert_eq!(suggestion.get_id(), "priority");
172
1
        assert_eq!(suggestion.get_secondary_value(), Some("high"));
173
1
    }
174
}