use crate::api::{ApiError, ZoomClient};
use crate::output::{self, OutputConfig};
#[allow(clippy::too_many_arguments)]
pub async fn meetings(
client: &mut ZoomClient,
out: &OutputConfig,
user: &str,
from: &str,
to: Option<&str>,
limit: Option<u32>,
offset: Option<u32>,
fields: Option<&[String]>,
) -> Result<(), ApiError> {
let result = client.list_user_meeting_reports(user, from, to).await?;
if out.json {
let mut items: Vec<serde_json::Value> = result
.meetings
.iter()
.map(|m| serde_json::to_value(m).expect("serialize"))
.collect();
if let Some(field_list) = fields {
items = items
.into_iter()
.map(|mut item| {
if let Some(obj) = item.as_object_mut() {
obj.retain(|k, _| field_list.iter().any(|f| f == k));
}
item
})
.collect();
}
let total = result.total_records.unwrap_or(items.len() as u64);
let offset_val = offset.unwrap_or(0) as usize;
let limited: Vec<serde_json::Value> = items
.into_iter()
.skip(offset_val)
.take(limit.unwrap_or(u32::MAX) as usize)
.collect();
let actual_limit = limit.unwrap_or(limited.len() as u32);
let envelope = serde_json::json!({
"items": limited,
"total": total,
"limit": actual_limit,
"offset": offset.unwrap_or(0)
});
out.print_data(&serde_json::to_string_pretty(&envelope).expect("serialize"));
} else {
if result.meetings.is_empty() {
out.print_message("No meetings found.");
return Ok(());
}
let rows: Vec<Vec<String>> = result
.meetings
.iter()
.map(|m| {
vec![
m.id.map(|id| id.to_string()).unwrap_or_default(),
m.topic.clone().unwrap_or_default(),
m.start_time
.as_deref()
.map(output::format_timestamp)
.unwrap_or_else(|| "-".into()),
m.duration
.map(|d| format!("{d} min"))
.unwrap_or_else(|| "-".into()),
m.participants_count
.map(|c| c.to_string())
.unwrap_or_else(|| "-".into()),
]
})
.collect();
out.print_data(&output::table(
&["ID", "TOPIC", "START TIME", "DURATION", "PARTICIPANTS"],
&rows,
));
if let Some(total) = result.total_records {
out.print_message(&format!("{total} meeting(s) total"));
}
}
Ok(())
}
pub async fn participants(
client: &mut ZoomClient,
out: &OutputConfig,
meeting_id: &str,
) -> Result<(), ApiError> {
let result = client.list_meeting_participant_reports(meeting_id).await?;
if out.json {
out.print_data(&serde_json::to_string_pretty(&result).expect("serialize"));
} else {
if result.participants.is_empty() {
out.print_message("No participants found.");
return Ok(());
}
let rows: Vec<Vec<String>> = result
.participants
.iter()
.map(|p| {
vec![
p.name.clone().unwrap_or_default(),
p.user_email.clone().unwrap_or_default(),
p.join_time
.as_deref()
.map(output::format_timestamp)
.unwrap_or_else(|| "-".into()),
p.duration
.map(|d| format!("{} min", d / 60))
.unwrap_or_else(|| "-".into()),
]
})
.collect();
out.print_data(&output::table(
&["NAME", "EMAIL", "JOIN TIME", "DURATION"],
&rows,
));
if let Some(total) = result.total_records {
out.print_message(&format!("{total} participant(s) total"));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::ZoomClient;
use wiremock::matchers::{method, path, query_param};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn test_out() -> OutputConfig {
OutputConfig::for_test()
}
#[tokio::test]
async fn reports_meetings_empty_is_ok() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v2/report/users/me/meetings"))
.and(query_param("from", "2026-04-01"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"meetings": [],
"total_records": 0,
"from": "2026-04-01",
"to": "2026-04-30"
})))
.mount(&server)
.await;
let mut client =
ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
meetings(
&mut client,
&test_out(),
"me",
"2026-04-01",
None,
None,
None,
None,
)
.await
.unwrap();
}
#[tokio::test]
async fn reports_meetings_renders_topic_and_duration() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v2/report/users/me/meetings"))
.and(query_param("from", "2026-04-01"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"meetings": [
{
"uuid": "abc==",
"id": 123456789,
"topic": "Weekly Standup",
"start_time": "2026-04-01T09:00:00Z",
"duration": 30,
"participants_count": 8
}
],
"total_records": 1,
"from": "2026-04-01",
"to": "2026-04-30"
})))
.mount(&server)
.await;
let mut client =
ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
let (out, buf) = OutputConfig::capturing();
meetings(
&mut client,
&out,
"me",
"2026-04-01",
None,
None,
None,
None,
)
.await
.unwrap();
let captured = buf.lock().unwrap().join("\n");
assert!(
captured.contains("Weekly Standup"),
"table must include topic"
);
assert!(captured.contains("30 min"), "table must include duration");
assert!(
captured.contains("8"),
"table must include participant count"
);
}
#[tokio::test]
async fn reports_participants_renders_name_email_and_duration() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v2/report/meetings/123456789/participants"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"participants": [
{
"name": "Alice",
"user_email": "alice@example.com",
"join_time": "2026-04-01T09:00:00Z",
"duration": 1800
}
],
"total_records": 1
})))
.mount(&server)
.await;
let mut client =
ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
let (out, buf) = OutputConfig::capturing();
participants(&mut client, &out, "123456789").await.unwrap();
let captured = buf.lock().unwrap().join("\n");
assert!(
captured.contains("Alice"),
"table must include participant name"
);
assert!(
captured.contains("alice@example.com"),
"table must include email"
);
assert!(captured.contains("30 min"), "1800s must render as 30 min");
}
}