1
use super::*;
2

            
3
// ── canned wire strings ────────────────────────────────────────────────────
4
// These mirror the format produced by rpc/src/natives/report.rs.
5

            
6
2
fn balance_wire_quoted(id: u64) -> String {
7
2
    let body = concat!(
8
        "(:balance-report :periods ((:label nil :roots ((",
9
        ":account-id \"00000000-0000-0000-0000-000000000000\" ",
10
        ":account-name \"Assets\" :account-path \"Assets\" :depth 0 ",
11
        ":account-type \"asset\" ",
12
        ":amounts ((:commodity-id \"00000000-0000-0000-0000-000000000000\" :symbol \"USD\" :amount 1000)) ",
13
        ":children ())))))"
14
    );
15
2
    let escaped = body.replace('\\', "\\\\").replace('"', "\\\"");
16
2
    format!("(:id {id} :value \"{escaped}\")")
17
2
}
18

            
19
1
fn activity_wire_quoted(id: u64) -> String {
20
1
    let body = concat!(
21
        "(:activity-report :meta nil :periods ((:label \"2026-Q1\" :groups ((",
22
        ":label \"Income\" :flip-sign t :roots ((",
23
        ":account-id \"00000000-0000-0000-0000-000000000000\" ",
24
        ":account-name \"Salary\" :account-path \"Income:Salary\" :depth 0 ",
25
        ":account-type nil ",
26
        ":amounts ((:commodity-id \"00000000-0000-0000-0000-000000000000\" :symbol \"USD\" :amount 1000)) ",
27
        ":children ())))))))"
28
    );
29
1
    let escaped = body.replace('\\', "\\\\").replace('"', "\\\"");
30
1
    format!("(:id {id} :value \"{escaped}\")")
31
1
}
32

            
33
1
fn breakdown_wire_quoted(id: u64) -> String {
34
1
    let body = concat!(
35
        "(:category-breakdown :meta nil :tag-name \"category\" :periods ((:label nil :rows ((",
36
        ":tag-value \"Food\" :uncategorized nil ",
37
        ":amounts ((:commodity-id \"00000000-0000-0000-0000-000000000000\" :symbol \"USD\" :amount 200))",
38
        ")))))"
39
    );
40
1
    let escaped = body.replace('\\', "\\\\").replace('"', "\\\"");
41
1
    format!("(:id {id} :value \"{escaped}\")")
42
1
}
43

            
44
1
fn error_wire(id: u64) -> String {
45
1
    format!("(:id {id} :error (:code server-error :message \"something went wrong\"))")
46
1
}
47

            
48
// ── on_reply tests ─────────────────────────────────────────────────────────
49

            
50
#[test]
51
1
fn on_reply_balance_produces_loaded_chart_spec() {
52
1
    let mut tab = ReportsTab::new();
53
1
    tab.set_loading(1, ReportKind::Balance, "bar");
54
1
    tab.on_reply(&balance_wire_quoted(1), ReportKind::Balance, "bar");
55
1
    assert!(
56
1
        matches!(tab.state, Fetch::Loaded(_)),
57
        "expected Loaded, got {:?}",
58
        tab.state
59
    );
60
1
    if let Fetch::Loaded(spec) = &tab.state {
61
1
        assert_eq!(spec.title, "Balance");
62
1
        assert!(!spec.series.is_empty(), "should have at least one series");
63
    }
64
1
}
65

            
66
#[test]
67
1
fn on_reply_activity_produces_loaded_chart_spec() {
68
1
    let mut tab = ReportsTab::new();
69
1
    tab.set_loading(2, ReportKind::Activity, "bar");
70
1
    tab.on_reply(&activity_wire_quoted(2), ReportKind::Activity, "bar");
71
1
    assert!(
72
1
        matches!(tab.state, Fetch::Loaded(_)),
73
        "expected Loaded, got {:?}",
74
        tab.state
75
    );
76
1
    if let Fetch::Loaded(spec) = &tab.state {
77
1
        assert_eq!(spec.title, "Activity");
78
    }
79
1
}
80

            
81
#[test]
82
1
fn on_reply_breakdown_produces_loaded_chart_spec() {
83
1
    let mut tab = ReportsTab::new();
84
1
    tab.set_loading(3, ReportKind::Breakdown, "bar");
85
1
    tab.on_reply(&breakdown_wire_quoted(3), ReportKind::Breakdown, "bar");
86
1
    assert!(
87
1
        matches!(tab.state, Fetch::Loaded(_)),
88
        "expected Loaded, got {:?}",
89
        tab.state
90
    );
91
1
    if let Fetch::Loaded(spec) = &tab.state {
92
1
        assert_eq!(spec.title, "Category Breakdown");
93
    }
94
1
}
95

            
96
#[test]
97
1
fn on_reply_uses_routed_kind_not_current_selection() {
98
    // The tab's selection has since moved to Activity, but a balance reply
99
    // routed with ReportKind::Balance must still parse as a balance chart.
100
1
    let mut tab = ReportsTab::new();
101
1
    tab.set_loading(9, ReportKind::Activity, "bar");
102
1
    tab.on_reply(&balance_wire_quoted(9), ReportKind::Balance, "bar");
103
1
    if let Fetch::Loaded(spec) = &tab.state {
104
1
        assert_eq!(spec.title, "Balance", "must parse with the routed kind");
105
    } else {
106
        panic!("expected Loaded, got {:?}", tab.state);
107
    }
108
1
}
109

            
110
#[test]
111
1
fn on_reply_error_wire_produces_error_state() {
112
1
    let mut tab = ReportsTab::new();
113
1
    tab.set_loading(4, ReportKind::Balance, "bar");
114
1
    tab.on_reply(&error_wire(4), ReportKind::Balance, "bar");
115
1
    assert!(
116
1
        matches!(tab.state, Fetch::Error(_)),
117
        "expected Error, got {:?}",
118
        tab.state
119
    );
120
1
    if let Fetch::Error(msg) = &tab.state {
121
1
        assert!(msg.contains("something went wrong"), "message: {msg}");
122
    }
123
1
}
124

            
125
#[test]
126
1
fn reset_returns_to_idle() {
127
1
    let mut tab = ReportsTab::new();
128
1
    tab.set_loading(5, ReportKind::Balance, "bar");
129
1
    tab.reset();
130
1
    assert!(matches!(tab.state, Fetch::Idle));
131
1
}
132

            
133
#[test]
134
1
fn set_loading_records_kind_and_chart() {
135
1
    let mut tab = ReportsTab::new();
136
1
    tab.set_loading(7, ReportKind::Activity, "line");
137
1
    assert_eq!(tab.kind, ReportKind::Activity);
138
1
    assert_eq!(tab.chart, "line");
139
1
    assert!(matches!(tab.state, Fetch::Loading { id: 7 }));
140
1
}
141

            
142
#[test]
143
1
fn report_kind_labels_are_non_empty() {
144
3
    for kind in [
145
1
        ReportKind::Balance,
146
1
        ReportKind::Activity,
147
1
        ReportKind::Breakdown,
148
1
    ] {
149
3
        assert!(!kind.label().is_empty());
150
    }
151
1
}