1use sonos_sdk::{Group, SonosSystem, Speaker};
2
3use crate::cli::GlobalFlags;
4use crate::config::Config;
5use crate::errors::CliError;
6
7pub fn resolve_speaker(
12 system: &SonosSystem,
13 config: &Config,
14 global: &GlobalFlags,
15) -> Result<Speaker, CliError> {
16 if let Some(group_name) = &global.group {
18 let g = system
19 .group(group_name)
20 .ok_or_else(|| CliError::GroupNotFound(group_name.to_string()))?;
21 return g
22 .coordinator()
23 .ok_or_else(|| CliError::GroupNotFound(group_name.to_string()));
24 }
25
26 if let Some(speaker_name) = &global.speaker {
27 return system
28 .speaker(speaker_name)
29 .ok_or_else(|| CliError::SpeakerNotFound(speaker_name.to_string()));
30 }
31
32 if let Some(default_group) = &config.default_group {
34 if let Some(g) = system.group(default_group) {
35 if let Some(coordinator) = g.coordinator() {
36 return Ok(coordinator);
37 }
38 }
39 }
40
41 system
43 .speakers()
44 .into_iter()
45 .next()
46 .ok_or_else(|| CliError::SpeakerNotFound("no speakers available".to_string()))
47}
48
49pub fn resolve_group(
54 system: &SonosSystem,
55 config: &Config,
56 global: &GlobalFlags,
57) -> Result<Group, CliError> {
58 if let Some(group_name) = &global.group {
59 return system
60 .group(group_name)
61 .ok_or_else(|| CliError::GroupNotFound(group_name.to_string()));
62 }
63
64 if let Some(default_group) = &config.default_group {
66 if let Some(g) = system.group(default_group) {
67 return Ok(g);
68 }
69 }
70
71 system
72 .groups()
73 .into_iter()
74 .next()
75 .ok_or_else(|| CliError::GroupNotFound("no groups available".to_string()))
76}
77
78pub fn require_speaker_only(
81 system: &SonosSystem,
82 global: &GlobalFlags,
83 command_name: &str,
84) -> Result<Speaker, CliError> {
85 if global.group.is_some() {
86 return Err(CliError::Validation(format!(
87 "--speaker is required for {command_name}"
88 )));
89 }
90 let name = global
91 .speaker
92 .as_deref()
93 .ok_or_else(|| CliError::Validation(format!("--speaker is required for {command_name}")))?;
94 system
95 .speaker(name)
96 .ok_or_else(|| CliError::SpeakerNotFound(name.to_string()))
97}
98
99#[cfg(all(test, feature = "test-helpers"))]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn resolve_speaker_by_name() {
105 let system = SonosSystem::with_speakers(&["Kitchen"]);
106 let config = Config::default();
107 let global = GlobalFlags {
108 speaker: Some("Kitchen".into()),
109 group: None,
110 quiet: false,
111 verbose: false,
112 no_input: false,
113 };
114 let spk = resolve_speaker(&system, &config, &global).unwrap();
115 assert_eq!(spk.name, "Kitchen");
116 }
117
118 #[test]
119 fn resolve_speaker_not_found() {
120 let system = SonosSystem::with_speakers(&["Kitchen"]);
121 let config = Config::default();
122 let global = GlobalFlags {
123 speaker: Some("Nonexistent".into()),
124 group: None,
125 quiet: false,
126 verbose: false,
127 no_input: false,
128 };
129 let result = resolve_speaker(&system, &config, &global);
130 assert!(matches!(result, Err(CliError::SpeakerNotFound(_))));
131 }
132
133 #[test]
134 fn resolve_speaker_falls_back_to_first() {
135 let system = SonosSystem::with_speakers(&["Kitchen"]);
136 let config = Config::default();
137 let global = GlobalFlags {
138 speaker: None,
139 group: None,
140 quiet: false,
141 verbose: false,
142 no_input: false,
143 };
144 let spk = resolve_speaker(&system, &config, &global).unwrap();
145 assert_eq!(spk.name, "Kitchen");
146 }
147
148 #[test]
149 fn resolve_speaker_empty_system_fails() {
150 let system = SonosSystem::with_speakers(&[]);
151 let config = Config::default();
152 let global = GlobalFlags {
153 speaker: None,
154 group: None,
155 quiet: false,
156 verbose: false,
157 no_input: false,
158 };
159 let result = resolve_speaker(&system, &config, &global);
160 assert!(result.is_err());
161 }
162
163 #[test]
164 fn require_speaker_only_rejects_group() {
165 let system = SonosSystem::with_speakers(&["Kitchen"]);
166 let global = GlobalFlags {
167 speaker: None,
168 group: Some("Living Room".into()),
169 quiet: false,
170 verbose: false,
171 no_input: false,
172 };
173 let result = require_speaker_only(&system, &global, "bass");
174 assert!(matches!(result, Err(CliError::Validation(_))));
175 }
176
177 #[test]
178 fn require_speaker_only_requires_speaker_flag() {
179 let system = SonosSystem::with_speakers(&["Kitchen"]);
180 let global = GlobalFlags {
181 speaker: None,
182 group: None,
183 quiet: false,
184 verbose: false,
185 no_input: false,
186 };
187 let result = require_speaker_only(&system, &global, "bass");
188 assert!(matches!(result, Err(CliError::Validation(_))));
189 }
190
191 #[test]
192 fn require_speaker_only_finds_speaker() {
193 let system = SonosSystem::with_speakers(&["Kitchen"]);
194 let global = GlobalFlags {
195 speaker: Some("Kitchen".into()),
196 group: None,
197 quiet: false,
198 verbose: false,
199 no_input: false,
200 };
201 let spk = require_speaker_only(&system, &global, "bass").unwrap();
202 assert_eq!(spk.name, "Kitchen");
203 }
204
205 #[test]
206 fn resolve_group_by_name() {
207 let system = SonosSystem::with_groups(&["Kitchen", "Bedroom"]);
208 let config = Config::default();
209 let global = GlobalFlags {
210 speaker: None,
211 group: Some("Kitchen".into()),
212 quiet: false,
213 verbose: false,
214 no_input: false,
215 };
216 let grp = resolve_group(&system, &config, &global).unwrap();
217 let coord = grp.coordinator().unwrap();
218 assert_eq!(coord.name, "Kitchen");
219 }
220
221 #[test]
222 fn resolve_group_not_found() {
223 let system = SonosSystem::with_groups(&["Kitchen"]);
224 let config = Config::default();
225 let global = GlobalFlags {
226 speaker: None,
227 group: Some("Nonexistent".into()),
228 quiet: false,
229 verbose: false,
230 no_input: false,
231 };
232 let result = resolve_group(&system, &config, &global);
233 assert!(matches!(result, Err(CliError::GroupNotFound(_))));
234 }
235
236 #[test]
237 fn resolve_group_falls_back_to_first() {
238 let system = SonosSystem::with_groups(&["Kitchen"]);
239 let config = Config::default();
240 let global = GlobalFlags {
241 speaker: None,
242 group: None,
243 quiet: false,
244 verbose: false,
245 no_input: false,
246 };
247 let grp = resolve_group(&system, &config, &global).unwrap();
248 let coord = grp.coordinator().unwrap();
249 assert_eq!(coord.name, "Kitchen");
250 }
251
252 #[test]
253 fn resolve_group_uses_config_default() {
254 let system = SonosSystem::with_groups(&["Kitchen", "Bedroom"]);
255 let config = Config {
256 default_group: Some("Bedroom".into()),
257 ..Config::default()
258 };
259 let global = GlobalFlags {
260 speaker: None,
261 group: None,
262 quiet: false,
263 verbose: false,
264 no_input: false,
265 };
266 let grp = resolve_group(&system, &config, &global).unwrap();
267 let coord = grp.coordinator().unwrap();
268 assert_eq!(coord.name, "Bedroom");
269 }
270
271 #[test]
272 fn resolve_group_empty_system_fails() {
273 let system = SonosSystem::with_groups(&[]);
274 let config = Config::default();
275 let global = GlobalFlags {
276 speaker: None,
277 group: None,
278 quiet: false,
279 verbose: false,
280 no_input: false,
281 };
282 let result = resolve_group(&system, &config, &global);
283 assert!(matches!(result, Err(CliError::GroupNotFound(_))));
284 }
285
286 #[test]
287 fn resolve_group_flag_wins_over_speaker() {
288 let system = SonosSystem::with_groups(&["Kitchen", "Bedroom"]);
289 let config = Config::default();
290 let global = GlobalFlags {
291 speaker: Some("Bedroom".into()),
292 group: Some("Kitchen".into()),
293 quiet: false,
294 verbose: false,
295 no_input: false,
296 };
297 let grp = resolve_group(&system, &config, &global).unwrap();
298 let coord = grp.coordinator().unwrap();
299 assert_eq!(coord.name, "Kitchen");
300 }
301}