1
//! Unit tests for the palette command form builder.
2

            
3
use super::{build_form, build_report_request};
4
use crate::tabs::reports::ReportKind;
5
use cli_core::{CommandError, CommandNode, command_tree, find_leaf};
6

            
7
13
fn args(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
8
13
    pairs
9
13
        .iter()
10
23
        .map(|(k, v)| (k.to_string(), v.to_string()))
11
13
        .collect()
12
13
}
13

            
14
26
fn path(segments: &[&str]) -> Vec<String> {
15
51
    segments.iter().map(|s| s.to_string()).collect()
16
26
}
17

            
18
19
fn leaf_node(path_segments: &[&str]) -> CommandNode {
19
19
    let tree = command_tree();
20
19
    let node = find_leaf(&tree, path_segments).expect("path must resolve in the canonical tree");
21
19
    CommandNode {
22
19
        name: node.name.clone(),
23
19
        comment: node.comment.clone(),
24
19
        is_leaf: true,
25
19
        subcommands: vec![],
26
19
        arguments: vec![],
27
19
    }
28
19
}
29

            
30
// ── version ──────────────────────────────────────────────────────────────────
31

            
32
#[test]
33
1
fn version_produces_version_form() {
34
1
    let node = leaf_node(&["version"]);
35
1
    let form = build_form(&node, &path(&["version"]), &[])
36
1
        .unwrap()
37
1
        .unwrap();
38
1
    assert_eq!(form, "(get-version)");
39
1
}
40

            
41
// ── account ──────────────────────────────────────────────────────────────────
42

            
43
#[test]
44
1
fn account_list_produces_list_accounts() {
45
1
    let node = leaf_node(&["account", "list"]);
46
1
    let form = build_form(&node, &path(&["account", "list"]), &[])
47
1
        .unwrap()
48
1
        .unwrap();
49
1
    assert_eq!(form, "(list-accounts)");
50
1
}
51

            
52
#[test]
53
1
fn account_balance_produces_get_balances() {
54
1
    let uuid = "550e8400-e29b-41d4-a716-446655440000";
55
1
    let node = leaf_node(&["account", "balance"]);
56
1
    let form = build_form(
57
1
        &node,
58
1
        &path(&["account", "balance"]),
59
1
        &args(&[("account", uuid)]),
60
    )
61
1
    .unwrap()
62
1
    .unwrap();
63
1
    assert_eq!(form, format!(r#"(get-balances "{uuid}")"#));
64
1
}
65

            
66
#[test]
67
1
fn account_balance_missing_uuid_is_error() {
68
1
    let node = leaf_node(&["account", "balance"]);
69
1
    assert!(build_form(&node, &path(&["account", "balance"]), &[]).is_err());
70
1
}
71

            
72
#[test]
73
1
fn account_balance_bad_uuid_is_error() {
74
1
    let node = leaf_node(&["account", "balance"]);
75
1
    assert!(
76
1
        build_form(
77
1
            &node,
78
1
            &path(&["account", "balance"]),
79
1
            &args(&[("account", "not-a-uuid")])
80
1
        )
81
1
        .is_err()
82
    );
83
1
}
84

            
85
#[test]
86
1
fn account_create_opens_modal_path() {
87
1
    let node = leaf_node(&["account", "create"]);
88
1
    let result = build_form(&node, &path(&["account", "create"]), &[]);
89
1
    assert!(result.is_ok(), "account create must not error");
90
1
    assert!(
91
1
        result.unwrap().is_none(),
92
        "account create must return Ok(None) for modal path"
93
    );
94
1
}
95

            
96
#[test]
97
1
fn account_create_with_args_still_modal_path() {
98
1
    let parent = "550e8400-e29b-41d4-a716-446655440001";
99
1
    let node = leaf_node(&["account", "create"]);
100
1
    let result = build_form(
101
1
        &node,
102
1
        &path(&["account", "create"]),
103
1
        &args(&[("name", "Sub"), ("parent", parent)]),
104
    );
105
1
    assert!(result.is_ok(), "account create must not error");
106
1
    assert!(
107
1
        result.unwrap().is_none(),
108
        "account create always returns Ok(None)"
109
    );
110
1
}
111

            
112
// ── commodity ────────────────────────────────────────────────────────────────
113

            
114
#[test]
115
1
fn commodity_list_produces_list_commodities() {
116
1
    let node = leaf_node(&["commodity", "list"]);
117
1
    let form = build_form(&node, &path(&["commodity", "list"]), &[])
118
1
        .unwrap()
119
1
        .unwrap();
120
1
    assert_eq!(form, "(list-commodities)");
121
1
}
122

            
123
#[test]
124
1
fn commodity_create_opens_modal_path() {
125
1
    let node = leaf_node(&["commodity", "create"]);
126
1
    let result = build_form(&node, &path(&["commodity", "create"]), &[]);
127
1
    assert!(result.is_ok(), "commodity create must not error");
128
1
    assert!(
129
1
        result.unwrap().is_none(),
130
        "commodity create must return Ok(None) for modal path"
131
    );
132
1
}
133

            
134
#[test]
135
1
fn commodity_convert_opens_modal_path() {
136
1
    let node = leaf_node(&["commodity", "convert"]);
137
1
    let result = build_form(&node, &path(&["commodity", "convert"]), &[]);
138
1
    assert!(result.is_ok(), "commodity convert must not error");
139
1
    assert!(
140
1
        result.unwrap().is_none(),
141
        "commodity convert must return Ok(None) for modal path"
142
    );
143
1
}
144

            
145
// ── transaction ───────────────────────────────────────────────────────────────
146

            
147
#[test]
148
1
fn transaction_list_no_account_produces_empty_string_arg() {
149
1
    let node = leaf_node(&["transaction", "list"]);
150
1
    let form = build_form(&node, &path(&["transaction", "list"]), &[])
151
1
        .unwrap()
152
1
        .unwrap();
153
1
    assert_eq!(form, r#"(list-transactions "")"#);
154
1
}
155

            
156
#[test]
157
1
fn transaction_list_with_account() {
158
1
    let uuid = "550e8400-e29b-41d4-a716-446655440002";
159
1
    let node = leaf_node(&["transaction", "list"]);
160
1
    let form = build_form(
161
1
        &node,
162
1
        &path(&["transaction", "list"]),
163
1
        &args(&[("account", uuid)]),
164
    )
165
1
    .unwrap()
166
1
    .unwrap();
167
1
    assert_eq!(form, format!(r#"(list-transactions "{uuid}")"#));
168
1
}
169

            
170
#[test]
171
1
fn transaction_list_malformed_account_errors() {
172
1
    let node = leaf_node(&["transaction", "list"]);
173
1
    let res = build_form(
174
1
        &node,
175
1
        &path(&["transaction", "list"]),
176
1
        &args(&[("account", "not-a-uuid")]),
177
    );
178
1
    assert!(
179
1
        matches!(res, Err(CommandError::Argument(_))),
180
        "malformed optional account uuid must error, got: {res:?}"
181
    );
182
1
}
183

            
184
#[test]
185
1
fn account_create_malformed_parent_is_modal_path() {
186
    // Validation is deferred to the form modal, so any args return Ok(None).
187
1
    let node = leaf_node(&["account", "create"]);
188
1
    let res = build_form(
189
1
        &node,
190
1
        &path(&["account", "create"]),
191
1
        &args(&[("name", "Cash"), ("parent", "bogus")]),
192
    );
193
1
    assert!(res.is_ok(), "account create must not error");
194
1
    assert!(res.unwrap().is_none(), "account create is a modal path");
195
1
}
196

            
197
#[test]
198
1
fn transaction_create_opens_modal_path() {
199
    // transaction create always opens the form modal regardless of args.
200
1
    let node = leaf_node(&["transaction", "create"]);
201
1
    let result = build_form(&node, &path(&["transaction", "create"]), &[]);
202
1
    assert!(result.is_ok(), "transaction create must not error");
203
1
    assert!(
204
1
        result.unwrap().is_none(),
205
        "transaction create must return Ok(None) for modal path"
206
    );
207
1
}
208

            
209
#[test]
210
1
fn transaction_create_with_args_still_modal_path() {
211
1
    let from = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
212
1
    let to = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb";
213
1
    let cur = "cccccccc-cccc-cccc-cccc-cccccccccccc";
214
1
    let node = leaf_node(&["transaction", "create"]);
215
1
    let result = build_form(
216
1
        &node,
217
1
        &path(&["transaction", "create"]),
218
1
        &args(&[
219
1
            ("from", from),
220
1
            ("to", to),
221
1
            ("from_currency", cur),
222
1
            ("to_currency", cur),
223
1
            ("value", "100"),
224
1
        ]),
225
    );
226
1
    assert!(result.is_ok(), "transaction create must not error");
227
1
    assert!(
228
1
        result.unwrap().is_none(),
229
        "transaction create always returns Ok(None)"
230
    );
231
1
}
232

            
233
// ── config ────────────────────────────────────────────────────────────────────
234

            
235
#[test]
236
1
fn config_get_produces_get_config() {
237
1
    let node = leaf_node(&["config", "get"]);
238
1
    let form = build_form(
239
1
        &node,
240
1
        &path(&["config", "get"]),
241
1
        &args(&[("name", "theme")]),
242
    )
243
1
    .unwrap()
244
1
    .unwrap();
245
1
    assert_eq!(form, r#"(get-config "theme")"#);
246
1
}
247

            
248
#[test]
249
1
fn config_get_missing_name_is_error() {
250
1
    let node = leaf_node(&["config", "get"]);
251
1
    assert!(build_form(&node, &path(&["config", "get"]), &[]).is_err());
252
1
}
253

            
254
#[test]
255
1
fn config_set_returns_none_for_modal_path() {
256
1
    let node = leaf_node(&["config", "set"]);
257
1
    assert!(
258
1
        build_form(&node, &path(&["config", "set"]), &[])
259
1
            .unwrap()
260
1
            .is_none()
261
    );
262
1
}
263

            
264
// ── build_report_request ─────────────────────────────────────────────────────
265

            
266
#[test]
267
1
fn reports_balance_with_no_dates_returns_request() {
268
1
    let req = build_report_request(&path(&["reports", "balance"]), &[])
269
1
        .unwrap()
270
1
        .unwrap();
271
1
    assert_eq!(req.kind, ReportKind::Balance);
272
1
    assert!(req.from.is_empty());
273
1
    assert!(req.to.is_empty());
274
1
    assert_eq!(req.chart, "bar");
275
1
}
276

            
277
#[test]
278
1
fn reports_balance_with_chart_arg() {
279
1
    let req = build_report_request(&path(&["reports", "balance"]), &args(&[("chart", "line")]))
280
1
        .unwrap()
281
1
        .unwrap();
282
1
    assert_eq!(req.chart, "line");
283
1
}
284

            
285
#[test]
286
1
fn reports_activity_with_dates_coerces_rfc3339() {
287
1
    let req = build_report_request(
288
1
        &path(&["reports", "activity"]),
289
1
        &args(&[("from", "2026-01-01"), ("to", "2026-03-31")]),
290
    )
291
1
    .unwrap()
292
1
    .unwrap();
293
1
    assert_eq!(req.kind, ReportKind::Activity);
294
1
    assert_eq!(req.from, "2026-01-01T00:00:00+00:00");
295
1
    assert_eq!(req.to, "2026-03-31T23:59:59+00:00");
296
1
}
297

            
298
#[test]
299
1
fn reports_activity_without_dates_returns_none_for_modal() {
300
1
    let result = build_report_request(&path(&["reports", "activity"]), &[]).unwrap();
301
1
    assert!(
302
1
        result.is_none(),
303
        "missing dates must yield None (open modal)"
304
    );
305
1
}
306

            
307
#[test]
308
1
fn reports_breakdown_with_dates_coerces_rfc3339() {
309
1
    let req = build_report_request(
310
1
        &path(&["reports", "breakdown"]),
311
1
        &args(&[("from", "2026-01-01"), ("to", "2026-06-30")]),
312
    )
313
1
    .unwrap()
314
1
    .unwrap();
315
1
    assert_eq!(req.kind, ReportKind::Breakdown);
316
1
    assert_eq!(req.from, "2026-01-01T00:00:00+00:00");
317
1
    assert_eq!(req.to, "2026-06-30T23:59:59+00:00");
318
1
}
319

            
320
#[test]
321
1
fn reports_breakdown_bad_date_is_error() {
322
1
    let result = build_report_request(
323
1
        &path(&["reports", "breakdown"]),
324
1
        &args(&[("from", "not-a-date"), ("to", "2026-06-30")]),
325
    );
326
1
    assert!(result.is_err(), "bad date must be an error");
327
1
}
328

            
329
#[test]
330
1
fn reports_activity_already_rfc3339_passes_through() {
331
1
    let from = "2026-01-01T00:00:00+00:00";
332
1
    let to = "2026-03-31T23:59:59+00:00";
333
1
    let req = build_report_request(
334
1
        &path(&["reports", "activity"]),
335
1
        &args(&[("from", from), ("to", to)]),
336
    )
337
1
    .unwrap()
338
1
    .unwrap();
339
1
    assert_eq!(req.from, from);
340
1
    assert_eq!(req.to, to);
341
1
}