use std::collections::HashMap;
use anyhow::{Context, Error};
use chrono::NaiveDateTime;
use crate::common::app_context::AppContext;
use crate::common::clap_ext::ArgMatchesExt;
use crate::http_client::{MantaClient, OpenApiResultExt};
use crate::output;
use manta_shared::types::api::image::GetImagesParams;
use manta_shared::types::dto::Image;
fn parse_images_params(cli_args: &clap::ArgMatches) -> GetImagesParams {
let limit = cli_args.limit_or_most_recent();
GetImagesParams {
id: cli_args.opt_string("id"),
pattern: cli_args.opt_string("pattern"),
since: cli_args.get_one::<NaiveDateTime>("since").copied(),
until: cli_args.get_one::<NaiveDateTime>("until").copied(),
limit,
}
}
fn wire_datetime(value: Option<NaiveDateTime>) -> Option<String> {
value.map(|v| v.format("%Y-%m-%dT%H:%M:%S").to_string())
}
pub async fn exec(
ctx: &AppContext<'_>,
token: &str,
cli_args: &clap::ArgMatches,
) -> Result<(), Error> {
let params = parse_images_params(cli_args);
let client = MantaClient::from_app_ctx(ctx, Some(token))?;
let site = client.site_name();
let since = wire_datetime(params.since);
let until = wire_datetime(params.until);
let (raw, safety_rows) = tokio::try_join!(
async {
client
.openapi
.get_images(
params.id.as_deref(),
params.limit.map(i32::from),
params.pattern.as_deref(),
since.as_deref(),
until.as_deref(),
site,
)
.await
.into_anyhow()
.await
},
async {
client
.openapi
.get_image_analysis(site)
.await
.into_anyhow()
.await
},
)?;
let images: Vec<Image> = raw
.into_iter()
.map(serde_json::from_value)
.collect::<Result<Vec<_>, _>>()
.context("Failed to deserialize IMS images list")?;
let safety: HashMap<String, bool> = safety_rows
.into_iter()
.map(|r| (r.image_id, r.safe_to_delete))
.collect();
let only_safe = cli_args.get_flag("only-safe-to-delete");
let only_unsafe = cli_args.get_flag("only-unsafe-to-delete");
let images: Vec<Image> = if only_safe || only_unsafe {
images
.into_iter()
.filter(|img| {
let safe = img.id.as_deref().and_then(|id| safety.get(id)).copied();
(!only_safe || safe == Some(true))
&& (!only_unsafe || safe == Some(false))
})
.collect()
} else {
images
};
output::image::print(&images, &safety);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn images_cmd() -> clap::Command {
crate::build::get::subcommand_get_images()
}
#[test]
fn parse_no_args() {
let matches = images_cmd().get_matches_from(["images"]);
let params = parse_images_params(&matches);
assert!(params.id.is_none());
assert!(params.pattern.is_none());
assert!(params.limit.is_none());
}
#[test]
fn parse_most_recent_sets_limit_to_one() {
let matches = images_cmd().get_matches_from(["images", "--most-recent"]);
let params = parse_images_params(&matches);
assert_eq!(params.limit, Some(1));
}
#[test]
fn parse_id() {
let matches = images_cmd().get_matches_from(["images", "--id", "abc-123"]);
let params = parse_images_params(&matches);
assert_eq!(params.id.as_deref(), Some("abc-123"));
}
#[test]
fn parse_bare_since_starts_at_midnight() {
let matches =
images_cmd().get_matches_from(["images", "--since", "2026-01-01"]);
let params = parse_images_params(&matches);
assert_eq!(
params.since,
Some("2026-01-01T00:00:00".parse().unwrap()),
"a bare --since covers the whole day from midnight"
);
assert!(params.until.is_none());
}
#[test]
fn parse_bare_until_covers_end_of_day() {
let matches =
images_cmd().get_matches_from(["images", "--until", "2026-01-01"]);
let params = parse_images_params(&matches);
assert_eq!(
params.until,
Some("2026-01-01T23:59:59".parse().unwrap()),
"a bare --until covers the whole day through 23:59:59"
);
}
#[test]
fn parse_full_timestamp_is_exact_on_both_bounds() {
let matches = images_cmd().get_matches_from([
"images",
"--since",
"2026-03-04T09:00:00",
"--until",
"2026-03-04T15:30:00",
]);
let params = parse_images_params(&matches);
assert_eq!(params.since, Some("2026-03-04T09:00:00".parse().unwrap()));
assert_eq!(params.until, Some("2026-03-04T15:30:00".parse().unwrap()));
}
#[test]
fn parse_since_and_until_together() {
let matches = images_cmd().get_matches_from([
"images",
"--since",
"2026-01-01",
"--until",
"2026-02-01",
]);
let params = parse_images_params(&matches);
assert!(params.since.is_some() && params.until.is_some());
}
#[test]
fn malformed_date_is_rejected_at_parse_time() {
let err = images_cmd()
.try_get_matches_from(["images", "--since", "notadate"])
.expect_err("'notadate' is not a date");
let msg = err.to_string();
assert!(
msg.contains("YYYY-MM-DD"),
"error should name the accepted formats; got: {msg}"
);
}
#[test]
fn wire_datetime_emits_full_iso_timestamp() {
let rendered = wire_datetime(Some("2026-01-01T00:00:00".parse().unwrap()));
assert_eq!(rendered.as_deref(), Some("2026-01-01T00:00:00"));
assert_eq!(wire_datetime(None), None);
}
}